1#!/usr/local/bin/python3.8 2"""Provide functionality relating to an app in a dock. 3 4 Allow info relating to a running app (e.g. icon, command line, 5 .desktop file location, running processes, open windows etc) to be 6 obtained from the information that libWnck provides 7 8 Provide a surface on which the application's icon and the running indicator 9 can be drawn 10 11 Ensure that the app's icon and indicator are always drawn correctly 12 according to the size and orientation of the panel 13 14 Provide visual feedback to the user when an app is launched by pulsating 15 the app's icon 16 17 Draw a highlight around the app's icon if it is the foreground app 18 19 Maintain a list of all of the app's running processes and their windows 20 21 Ensure that the application's windows visually minimise to the 22 application's icon on the dock 23""" 24 25# 26# Copyright (C) 1997-2003 Free Software Foundation, Inc. 27# 28# This program is free software; you can redistribute it and/or 29# modify it under the terms of the GNU General Public License as 30# published by the Free Software Foundation; either version 2 of the 31# License, or (at your option) any later version. 32# 33# This program is distributed in the hope that it will be useful, but 34# WITHOUT ANY WARRANTY; without even the implied warranty of 35# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 36# General Public License for more details. 37# 38# You should have received a copy of the GNU General Public License 39# along with this program; if not, write to the Free Software 40# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 41# 02110-1301, USA. 42# 43# Author: 44# Robin Thompson 45 46# do not change the value of this variable - it will be set during build 47# according to the value of the --with-gtk3 option used with .configure 48build_gtk2 = False 49 50import gi 51 52if build_gtk2: 53 gi.require_version("Gtk", "2.0") 54 gi.require_version("Wnck", "1.0") 55else: 56 gi.require_version("Gtk", "3.0") 57 gi.require_version("Wnck", "3.0") 58 59gi.require_version("MatePanelApplet", "4.0") 60gi.require_version("Bamf", "3") 61 62from gi.repository import Gtk 63from gi.repository import MatePanelApplet 64from gi.repository import Gdk 65from gi.repository import GdkPixbuf 66from gi.repository import Wnck 67from gi.repository import Gio 68from gi.repository import GObject 69from gi.repository import GLib 70from gi.repository import Bamf 71 72import cairo 73import math 74import xdg.DesktopEntry as DesktopEntry 75import xdg.BaseDirectory as BaseDirectory 76import os 77import os.path 78import subprocess 79import re 80import colorsys 81 82from collections import namedtuple 83 84import dock_prefs 85from docked_app_helpers import * 86import window_control 87 88from log_it import log_it as log_it 89 90ColorTup = namedtuple('ColorTup', ['r', 'g', 'b']) 91 92 93def get_backlight_color(pixbuf): 94 """ 95 96 Read all of the pixel values in a pixbuf and calculate an appropriate 97 colour to use as an icon backlight 98 99 Code adapated from Unity desktop (https://code.launchpad.net/~unity-team/unity/trunk) 100 specifically from LauncherIcon::ColorForIcon in LauncherIcon.cpp 101 102 Args: 103 pixbuf : a pixbuf object containing the image 104 105 Returns: 106 a tuple of r,g,b value (0-255) 107 """ 108 109 width = pixbuf.props.width 110 rowstride = pixbuf.props.rowstride 111 height = pixbuf.props.height 112 113 num_channels = pixbuf.get_n_channels() 114 has_alpha = pixbuf.get_has_alpha() 115 116 img = pixbuf.get_pixels() 117 r_total = g_total = b_total = 0 118 total = 0.0 119 120 for w_count in range(width): 121 for h_count in range(height): 122 pix_index = (h_count * rowstride + w_count * num_channels) 123 pix_r = img[pix_index] 124 pix_g = img[pix_index + 1] 125 pix_b = img[pix_index + 2] 126 if has_alpha: 127 pix_a = img[pix_index + 3] 128 else: 129 pix_a = 255 130 131 saturation = float(max(pix_r, max(pix_g, pix_b)) - min(pix_r, min(pix_g, pix_b))) / 255.0 132 relevance = .1 + .9 * (float(pix_a) / 255) * saturation 133 134 r_total += (pix_r * relevance) % 256 135 g_total += (pix_g * relevance) % 256 136 b_total += (pix_b * relevance) % 256 137 total += relevance * 255.0 138 139 r = r_total / total 140 g = g_total / total 141 b = b_total / total 142 143 h, s, v = colorsys.rgb_to_hsv(r, g, b) 144 if s > 0.15: 145 s = 0.65 146 147 v = 0.6666 148 # Note: Unty uses v = 0.9, but this produces a very bright value which 149 # is reduced elsewhere. We use 0.6666 to reduce it here 150 br, bg, bb = colorsys.hsv_to_rgb(h, s, v) 151 152 v = 1.0 153 gr, gg, gb = colorsys.hsv_to_rgb(h, s, v) 154 155 return int(br * 255), int(bg * 255), int(bb * 255) 156 157 158def get_avg_color(pixbuf): 159 """calculate the average colour of a pixbuf. 160 161 Read all of the pixel values in a pixbuf (excluding those which are below a 162 certain alpha value) and calculate the average colour of all the contained 163 colours 164 165 Args: 166 pixbuf : a pixbuf object containing the image 167 168 Returns: 169 a tuple of r,g,b values (0-255) 170 """ 171 172 width = pixbuf.props.width 173 rowstride = pixbuf.props.rowstride 174 height = pixbuf.props.height 175 has_alpha = pixbuf.get_has_alpha() 176 pixels = pixbuf.get_pixels() 177 nchannels = pixbuf.get_n_channels() 178 # convert the pixels into an rgb array with alpha channel 179 data = [] 180 for y_count in range(height - 1): 181 x_count = 0 182 while x_count < (width * nchannels): 183 pix_red = pixels[x_count + (rowstride * y_count)] 184 pix_green = pixels[x_count + 1 + (rowstride * y_count)] 185 pix_blue = pixels[x_count + 2 + (rowstride * y_count)] 186 if has_alpha: 187 pix_alpha = pixels[x_count + 3 + (rowstride * y_count)] 188 else: 189 pix_alpha = 255 190 191 data.append([pix_red, pix_green, pix_blue, pix_alpha]) 192 193 x_count += nchannels 194 195 red = 0 196 green = 0 197 blue = 0 198 num_counted = 0 199 200 for pixels in range(len(data)): 201 if data[pixels][3] > 200: # only count pixel if alpha above this 202 # level 203 red += data[pixels][0] 204 green += data[pixels][1] 205 blue += data[pixels][2] 206 num_counted += 1 207 208 if num_counted > 0: 209 ravg = int(red / num_counted) 210 gavg = int(green / num_counted) 211 bavg = int(blue / num_counted) 212 else: 213 # in case of a bad icon assume a grey average colour 214 # this should fix a division by zero error that occurred at this point 215 # in the code, but which I've only seen once and never been able to 216 # duplicate 217 ravg = gavg = bavg = 128 218 219 return ravg, gavg, bavg 220 221 222CONST_PULSE_STEPS = 20 223CONST_PULSE_DELAY = 40 224 225 226class ScrollType: 227 """ Class to define the ways in which the docked apps may scroll in the dock""" 228 SCROLL_NONE = 0 229 SCROLL_UP = 1 # for horizontal applets 'up' equates to 'left' 230 SCROLL_DOWN = 2 # or, for horizontal applets, right... 231 232 233class PulseTimer(object): 234 """Class to help provide feedback when a user launches an app from the dock. 235 236 Instantiates a timer which periodically redraws an application in the dock 237 at various transparency levels until the timer has been run a certain 238 number of times 239 240 Attributes: 241 app = the DockedApp object which we want to pulsate 242 timer_id = the id of the timer that is instantiated 243 244 """ 245 246 def __init__(self, app, once_only=False): 247 """Init for the PulseTimer class. 248 249 Sets everything up by creating the timer, setting a reference to the 250 DockedApp and telling the app that it is pulsing 251 252 Arguments: 253 app : the DockedApp object 254 once_only : do only one iteration of pulsing, then stop 255 """ 256 257 self.timer_count = 0 258 self.app = app 259 self.app.pulse_step = 0 260 self.app.is_pulsing = True 261 self.once_only = once_only 262 self.timer_id = GObject.timeout_add(CONST_PULSE_DELAY, self.do_timer) 263 264 def do_timer(self): 265 """The timer function. 266 267 Increments the number of times the time function has been called. If it 268 hasn't reached the maximum number, increment the app's pulse counter. 269 If the maximum number has been reached, stop the app pulsing and 270 delete the timer. 271 272 Redraw the app's icon 273 """ 274 275 # the docked app may indicate it no longer wants to pulse... 276 if not self.app.is_pulsing: 277 self.remove_timer() 278 self.app.queue_draw() 279 return False 280 281 self.timer_count += 1 282 if self.timer_count / int(1000 / CONST_PULSE_DELAY) == 45: 283 # we've been pulsing for long enough, the user will be getting a headache 284 self.remove_timer() 285 self.app.queue_draw() 286 return False 287 288 if self.app.pulse_step != CONST_PULSE_STEPS: 289 self.app.pulse_step += 1 290 else: 291 # if we're starting the app for the first time (with startup notification) 292 # and it still hasn't finished loading, carry on pulsing 293 if (self.app.startup_id is not None) and not self.once_only: 294 self.app.pulse_step = 0 295 else: 296 # we only want the icon to pulse once 297 # if we have a startup_id the notification needs to be cancelled 298 if self.app.startup_id is not None: 299 self.app.cancel_startup_notification() 300 301 self.remove_timer() 302 303 self.app.queue_draw() 304 return True 305 306 def remove_timer(self): 307 """ 308 Cancel the timer and stop the app icon pulsing... 309 310 """ 311 312 self.app.is_pulsing = False 313 GObject.source_remove(self.timer_id) 314 315 316CONST_BLINK_DELAY = 330 317 318 319class AttentionTimer(object): 320 """Class to help provide visual feedback when an app requries user attention. 321 322 Instantiates a timer which periodically checks whether or not the app 323 still needs attention. If the app is blinking, it toggles the 324 blink state 325 on and off until the app no longer needs attention 326 327 Attributes: 328 app = the DockedApp object that needs attentions 329 timer_id = the id of the timer that is instantiated 330 331 """ 332 333 def __init__(self, app): 334 """Init for the AttentionTimer class. 335 336 Sets everything up by creating the timer, setting a reference to the 337 DockedApp and setting the inital flash state to off 338 339 Arguments: 340 app : the DockedApp object 341 """ 342 343 self.app = app 344 self.app.needs_attention = True 345 self.app.attention_blink_on = False 346 self.timer_id = GObject.timeout_add(CONST_BLINK_DELAY, self.do_timer) 347 348 # make the app redraw itself 349 app.queue_draw() 350 351 def do_timer(self): 352 """The timer function. 353 354 If the app no longer needs attention, stop it flashing and delete 355 the timer. Otherwise, invert the flash. 356 357 Finally,Redraw the app's icon 358 """ 359 360 if self.app.needs_attention: 361 self.app.attention_blink_on = not self.app.attention_blink_on 362 else: 363 GObject.source_remove(self.timer_id) 364 365 self.app.queue_draw() 366 367 return True 368 369 370class DockedApp(object): 371 """Provide a docked app class 372 373 Attributes: 374 375 bamf_app : the Bamf.Applications related to the running app 376 app_name : e.g. Google Chrome, used for tooltips and the applet 377 right click menu etc 378 rc_actions : A list of strings containing the names of the additional application 379 actions suppported by the app 380 cmd_line : the command line and arguments used to start the app 381 icon_name : name of the app icon 382 icon_filename : the filename of the app icon 383 desktop_file : the filename of the app's .desktop file 384 desktop_ai : a Gio.GDesktopAppInfo read from the .desktop file 385 startup_id : id used for startup notifications 386 applet_win : the Gdk.Window of the panel applet 387 applet : the panel applet 388 applet_orient : the applet orientation 389 390 drawing_area: Gtk.Label- provides a surface on which the app icon can 391 be drawn 392 drawing_area_size : the base size in pixels (height AND width) that we have 393 to draw in - note that some indicators require more 394 and must specify this... 395 is_pulsing : boolean - True if the app is pulsing 396 pulse_step : a count of how far we are through the pulse animation 397 app_pb : a pixbuf of the app's icon 398 app_surface : a surface of the app's icon 399 highlight_colour : ColorTup of the colours used to highlight the app 400 when it is foreground 401 is_active : boolean - True = the app is the foreground app 402 has_mouse : boolean - True = the mouse is over the app's icon 403 is_pinned : boolean - Whether or not the app is pinned to the dock 404 indicator : the type of indictor (e.g. light or dark) to draw under 405 running apps 406 active_bg : the type of background (e.g. gradient or solid fill) to 407 be drawn when the app is the active app 408 ind_ws : wnck_workspace or None - if set, indicators are to be 409 drawn for windows on the specified workspace 410 last_active_win : the Bamf.Window of the app's last active window 411 412 is_dragee : boolean - indicates whether or not the app's icon is 413 being dragged to a new position on the dock 414 415 show_progress : boolean - indicates whether or not to display a 416 progress indicator on the app's icon 417 progress_val : the progress value( 0 to 1.0) 418 show_count : boolean - indicates whether or not to display a 419 count value on the app's icon 420 count_val : the value of the count 421 needs_attention: whether or not the app needs the user's attention 422 attention_type : how the docked app indicates to the user that the app 423 needs attention 424 attention_blink_on : when an app blinks when it needs attention, this specfies 425 the state 426 scroll_dir : indicates the way that the dock may be scrolled (if any) 427 if the mouse hovers over this app. Also used to draw the 428 app icon in such a way as to indicate that scrolling is available 429 """ 430 431 def __init__(self): 432 """ Init for the DockApplet class. 433 434 Create a surface to draw the app icon on 435 Set detault values 436 """ 437 438 super().__init__() 439 440 self.bamf_app = None 441 self.app_info = [] 442 self.app_name = "" 443 self.rc_actions = [] 444 self.cmd_line = "" 445 self.icon_name = "" 446 self.icon_filename = "" 447 self.desktop_file = "" 448 self.desktop_ai = None 449 self.icon_geometry_set = False 450 self.applet_win = None 451 self.applet_orient = None 452 self.ind_ws = None 453 self.startup_id = None 454 self.applet = None 455 456 # all drawing is done to a Gtk.Label rather than e.g. a drawing area 457 # or event box this allows panel transparency/custom backgrounds to be 458 # honoured 459 # However, the downside of this is that mouse events cannot be handled 460 # by this class and instead have to be done by the applet itself 461 462 self.drawing_area = Gtk.Label() 463 self.drawing_area.set_app_paintable(True) 464 self.drawing_area_size = 0 465 466 self.is_pulsing = False 467 self.pulse_step = 0 468 469 self.needs_attention = False 470 self.attention_blink_on = False 471 472 self.app_pb = None 473 self.app_surface = None 474 self.highlight_color = ColorTup(r=0.0, g=0.0, b=0.0) 475 476 self.is_active = False 477 self.has_mouse = False 478 479 self.is_pinned = False 480 481 # set defaults 482 self.indicator = IndicatorType.LIGHT # light indicator 483 self.multi_ind = False # single indicator 484 self.active_bg = IconBgType.GRADIENT 485 486 self.attention_type = dock_prefs.AttentionType.BLINK 487 488 self.last_active_win = None 489 490 # set up event handler for the draw/expose event 491 if build_gtk2: 492 self.drawing_area.connect("expose-event", self.do_expose_event) 493 else: 494 self.drawing_area.connect("draw", self.do_expose_event) 495 496 self.is_dragee = False 497 498 self.show_progress = False 499 self.progress_val = 0.0 500 self.show_count = False 501 self.count_val = 0 502 503 self.scroll_dir = ScrollType.SCROLL_NONE 504 505 def set_bamf_app(self, b_app): 506 """ Sets the Bamf.Application related to this docked app 507 508 Params: b_app : the Bamf.Application to be added 509 """ 510 511 self.bamf_app = b_app 512 513 def clear_bamf_app(self): 514 """ Unsets the Bamf.Application related to this docked app 515 516 Params: b_app : the Bamf.Application to removed """ 517 518 self.bamf_app = None 519 520 def has_bamf_app(self, b_app): 521 """ Returns True if b_app is associated with this docked_app, False otherwise 522 523 Params: b_app - a Bamf.Application 524 """ 525 526 return b_app == self.bamf_app 527 528 def get_windows(self): 529 """ Convenience function to return a list of the app's Bamf.Windows 530 531 Returns : an empty list if the app is not running or if self.bamf_app is None, 532 otherwise the window list 533 534 """ 535 536 ret_val = [] 537 if (self.bamf_app is not None) and (self.bamf_app.is_running() or self.bamf_app.is_starting()): 538 ret_val = self.bamf_app.get_windows() 539 540 return ret_val 541 542 def get_first_normal_win(self): 543 """ Returns the app's first 'normal' window i.e. a window or dialog 544 545 Returns: 546 a Bamf.Window, or None 547 """ 548 549 if (self.bamf_app is not None) and (self.bamf_app.is_running()): 550 for win in self.get_windows(): 551 win_type = win.get_window_type() 552 if win_type in [Bamf.WindowType.NORMAL, Bamf.WindowType.DIALOG] or win.is_user_visible(): 553 return win 554 555 return None 556 557 def has_wnck_app(self, wnck_app): 558 """ see if this app has a process with the specified wnck_app 559 560 Returns True if the wnck_app is found, False otherwise 561 """ 562 563 ret_val = False 564 for aai in self.app_info: 565 if aai.app == wnck_app: 566 ret_val = True 567 break 568 569 return ret_val 570 571 def has_bamf_window(self, win): 572 """ 573 Checks to see if a window belongs to the app 574 575 Params: 576 win : the Bamf.Window we're interested in 577 578 Returns: 579 True if the window belongs to the app, False otherwise 580 581 """ 582 583 windows = self.get_windows() 584 return win in windows 585 586 def setup_from_bamf(self, app_match_list): 587 """ Setup an already running app using info from self.bamf_app 588 589 This is only called when bamf cannot match an app with it's .desktop file, 590 so we can also do some extra checking from the list of hard to match 591 .desktop files 592 593 """ 594 595 # get a list of all the possible locations of .desktop files 596 data_dirs = BaseDirectory.xdg_data_dirs 597 app_dirs = [] 598 for dir in data_dirs: 599 app_dirs.append(os.path.join(dir, "applications/")) 600 601 # search for a match in 602 for app in app_match_list: 603 if self.bamf_app.get_name() == app[0]: 604 605 for dir in app_dirs: 606 desktop_file = os.path.join(dir, app[2]) 607 if os.path.exists(desktop_file): 608 self.desktop_file = desktop_file 609 if self.read_info_from_desktop_file(): 610 return 611 612 # no match, so just get basic info 613 self.app_name = self.bamf_app.get_name() 614 self.icon_name = "wnck" # indicate we want to get the app icon from wnck 615 616 def set_app_name(self, app_name): 617 """sets the app name. 618 619 Stores the entire name, which may or may not also contain a 620 document title or other app specific info. This will need to 621 be parsed when necessary to obtain the actual app name 622 623 Args: The app name 624 625 """ 626 627 self.app_name = app_name 628 629 def set_urgency(self, urgent): 630 """ Sets the app's urgency state 631 632 Params : urgent - bool, whether or not the app is signalling urgency 633 """ 634 635 if urgent: 636 if not self.needs_attention: 637 self.needs_attention = True 638 self.attention_blink_on = False # initial blink state = off 639 timer = AttentionTimer(self) 640 641 if not self.is_visible(): 642 self.show_icon() 643 else: 644 if self.needs_attention: 645 # we need to turn flashing off 646 self.needs_attention = False 647 self.queue_draw() 648 649 # the timer will handle the rest .... 650 # hiding the icon (if necessary) will be taken care of next 651 # time the user changes workspace 652 653 def get_cmdline_from_pid(self, pid): 654 """ Find the command line and arguments used to launch the app 655 656 Use the ps command to return the command line and arguments 657 for the specified pid 658 659 Set self.path to the full command line 660 661 Args: 662 pid - a process id 663 664 """ 665 666 cmdstr = "xargs -0 < /proc/%d/cmdline" % pid 667 668 cmd = subprocess.Popen(cmdstr, shell=True, stdout=subprocess.PIPE) 669 670 for line in cmd.stdout: 671 pass 672 673 if line is not None: 674 self.cmd_line = line.decode("utf-8") 675 676 def has_windows_on_workspace(self, wnck_workspace): 677 """ test whether the app has at least one window open on a specified 678 workspace 679 680 Args: 681 wnck_workspace - the workspace to check for 682 683 Returns: 684 boolean 685 """ 686 687 for win in self.get_windows(): 688 wnck_win = Wnck.Window.get(win.get_xid()) 689 if wnck_win is not None: 690 win_ws = wnck_win.get_workspace() 691 if win_ws == wnck_workspace: 692 return True 693 694 return False 695 696 def has_unminimized_windows(self): 697 """ test whether the app has at least one unminimized window 698 699 Returns: 700 boolean 701 """ 702 703 win_list = self.get_windows() 704 for win in win_list: 705 wnck_win = Wnck.Window.get(win.get_xid()) 706 if (wnck_win is not None) and (not wnck_win.is_minimized()): 707 return True 708 709 return False 710 711 def hide_icon(self): 712 """ Hides the app's icon""" 713 714 self.drawing_area.set_visible(False) 715 716 def show_icon(self): 717 """ Shows the app's icon""" 718 719 self.drawing_area.set_visible(True) 720 721 def is_visible(self): 722 """ Method which returns whether or not the app's icon is visible 723 724 Returns: 725 boolean 726 """ 727 return self.drawing_area.get_visible() 728 729 def get_desktop_from_custom_launcher(self, srch_dir): 730 """ Search the custom launchers in a specified directory for 731 one where the Exec field is found within self.cmd_line 732 733 If a match is found found, self.desktop_file is set accordingly 734 735 Note: All custom launchers .desktop filenames start 736 with "mda_" 737 738 Args: 739 srch_dir : the directory to search 740 741 Returns: 742 True if a match was found, False otherwise 743 """ 744 745 # TODO: replace DesktopEntry with Gio.DesktopAppInfo... and then 746 747 # if the search dir doesn't exist, don't do anything 748 if os.path.isdir(srch_dir) is False: 749 return False 750 751 for the_file in os.listdir(srch_dir): 752 if (the_file.startswith("mda_")) and \ 753 (the_file.endswith(".desktop")): 754 the_de = DesktopEntry.DesktopEntry(srch_dir + the_file) 755 756 # remove command line args from the Exec field of the .desktop 757 de_exec = the_de.getExec().split(None, 1)[0] 758 759 if self.cmd_line.find(de_exec) != -1: 760 self.desktop_file = srch_dir + the_file 761 return True 762 763 def set_all_windows_icon_geometry(self, x, y, width, height): 764 """Set the location on screen where all of the app's windows will be 765 minimised to. 766 767 Args: 768 x : The X position in root window coordinates 769 y : The Y position in root window coordinates 770 width: the width of the minimise location 771 height: the height of the minimise location 772 773 """ 774 775 for win in self.get_windows(): 776 window_control.set_minimise_target(win, x, y, width, height) 777 778 return True 779 780 def get_allocation(self): 781 """ Returns the allocated position and size of the app's icon within the applet 782 """ 783 784 alloc = self.drawing_area.get_allocation() 785 return alloc.x, alloc.y, alloc.width, alloc.height 786 787 def set_drawing_area_size(self, size): 788 """Set the size request of the app's drawing area. 789 790 Args : 791 size : the size in pixels we need, although extra can be applied if required by indicators 792 793 """ 794 self.drawing_area_size = size 795 796 extra_s = ind_extra_s(self.indicator) 797 if extra_s == 0: 798 self.drawing_area.set_size_request(size, size) 799 else: 800 # we need to allocate extra space in the x or y dimension depending on applet orientation 801 if (self.applet_orient == MatePanelApplet.AppletOrient.DOWN) or \ 802 (self.applet_orient == MatePanelApplet.AppletOrient.UP): 803 self.drawing_area.set_size_request(size + extra_s, size) 804 else: 805 self.drawing_area.set_size_request(size, size + extra_s) 806 807 def queue_draw(self): 808 """Queue the app's icon to be redrawn. 809 """ 810 self.drawing_area.queue_draw() 811 812 def set_indicator(self, indicator): 813 """Set the running indicator type to the value specified 814 815 Args: 816 indicator - the indicator type 817 """ 818 self.indicator = indicator 819 820 def set_active_bg(self, active_bg): 821 """Set the active background type to the value specified 822 823 Args: 824 active_bg - the background type 825 826 """ 827 828 self.active_bg = active_bg 829 830 def set_multi_ind(self, multi_ind): 831 """ Set whether to use an indicator for each open window 832 833 Args: 834 multi_ind - boolean 835 """ 836 self.multi_ind = multi_ind 837 838 def set_attention_type(self, attention_type): 839 """Set the attention type to the value specified 840 841 Args: 842 indicator - the indicator type 843 """ 844 self.attention_type = attention_type 845 846 def is_running(self): 847 """ 848 Is the app running ? 849 850 Returns: True if the app is running, False if not 851 852 """ 853 854 if self.bamf_app is None: 855 return False 856 857 return self.bamf_app.is_running() 858 859 def has_desktop_file(self): 860 """ Does the app have a .desktop file? 861 862 Returns: True if there is a desktop file, False otherwise 863 """ 864 865 return self.desktop_file is not None 866 867 def read_info_from_desktop_file(self): 868 """Attempt to read from read the app's desktop file. 869 870 Will try to read the icon name and app name from the desktop file 871 Will also get the executeable path if we don't already have this 872 Will read the details of any right click menu options the .desktop 873 file defines 874 875 Returns: 876 True if successful, False otherwise 877 """ 878 879 if self.desktop_file: 880 if os.path.isabs(self.desktop_file): 881 self.desktop_ai = Gio.DesktopAppInfo.new_from_filename(self.desktop_file) 882 else: 883 self.desktop_ai = Gio.DesktopAppInfo.new(self.desktop_file) 884 885 self.app_name = self.desktop_ai.get_locale_string("Name") 886 self.icon_name = self.desktop_ai.get_string("Icon") 887 888 # if the desktop file does not specify an icon name, use the app 889 # name instead 890 if (self.icon_name is None) or (self.icon_name == ""): 891 self.icon_name = self.app_name.lower() 892 893 # hack for the MATE application browser app, where the 894 # .desktop file on Ubuntu does not specify an icon 895 if self.icon_name == "application browser": 896 self.icon_name = "computer" 897 898 # get the command specified in the .desktop file used to launch the app 899 self.cmd_line = self.desktop_ai.get_string("Exec") 900 901 # get the list of addtional application actions (to be activated by right 902 # clicking the app's dock icon) 903 self.rc_actions = self.desktop_ai.list_actions() 904 905 return True 906 907 return False 908 909 def app_has_custom_launcher(self): 910 """ Determines whether the docked app has a custom launcher 911 912 Examine the .desktop filename. If it starts with 913 "~/.local/share/applications/mda_" the app has a custom launcher 914 915 Returns : True if the app has a custom launcher, False otherwise 916 """ 917 918 cl_start = os.path.expanduser("~/.local/share/applications/mda_") 919 return os.path.expanduser(self.desktop_file).beginswith(cl_start) 920 921 def win_state_changed(self, wnck_win, changed_mask, new_state): 922 """Handler for the wnck_window state-changed event 923 924 If the app needs attention and we're not already flashing the icon 925 start it flashing. If the app icon is not visible, make it visible 926 927 If the app doesn't need attention and its icon is flashing, stop 928 it flashing 929 930 """ 931 932 if ((new_state & Wnck.WindowState.DEMANDS_ATTENTION) != 0) or\ 933 ((new_state & Wnck.WindowState.URGENT) != 0): 934 935 if not self.needs_attention: 936 self.needs_attention = True 937 self.attention_blink_on = False # initial blink state = off 938 timer = AttentionTimer(self) 939 940 if not self.is_visible(): 941 self.show_icon() 942 943 else: 944 if self.needs_attention: 945 # we need to turn flashing off 946 self.needs_attention = False 947 self.queue_draw() 948 949 # the timer will handle the rest .... 950 # hiding the icon (if necessary) will be taken care of next 951 # time the user changes workspace 952 953 def get_num_windows(self, cur_ws=None): 954 """ 955 Get the number of normal and dialog windows the app has open. 956 If cur_ws is specfied, then only windows on the specified workspace are counted 957 958 Params: cur_ws - an int representing the workspace number: 959 960 Returns: an int 961 962 """ 963 964 num_win = 0 965 966 if self.bamf_app is not None: 967 for win in self.get_windows(): 968 win_type = win.get_window_type() 969 if win_type in [Bamf.WindowType.NORMAL, Bamf.WindowType.DIALOG] and win.is_user_visible(): 970 if cur_ws is None: 971 num_win += 1 972 else: 973 xid = win.get_xid() 974 wnck_win = Wnck.Window.get(xid) 975 if (wnck_win is not None) and wnck_win.is_on_workspace(cur_ws): 976 num_win += 1 977 return num_win 978 979 def do_expose_event(self, drawing_area, event): 980 """The main drawing event for the docked app. 981 982 Does the following: 983 draw the app icon 984 if the mouse is over the app icon, highlight the icon 985 if the is running draw the app running indicators(according to the 986 applet orientation) 987 if the app is the foreground app, highlight the background with a 988 gradient fill 989 if the app is pulsing, draw the icon with a variable level of 990 transparency according to the pulse count 991 if the app is flashing, draw the icon either fully opaque or 992 completely transparent according to its flash state 993 if the app is being dragged to a new position on the dock, draw 994 a completely transparent background 995 Args: 996 drawing_area : the drawing area that related to the event. Will 997 always be the same as self.drawing area 998 event : in Gtk2 the event arguments, in Gtk3 a cairo context 999 to draw on 1000 1001 """ 1002 1003 # there are lots of drawing operations to be done, so do them to an 1004 # offscreen surface and when all is finished copy this to the docked 1005 # app 1006 1007 if self.applet_orient == MatePanelApplet.AppletOrient.DOWN or \ 1008 self.applet_orient == MatePanelApplet.AppletOrient.UP: 1009 oss_w = self.drawing_area_size + ind_extra_s(self.indicator) 1010 oss_h = self.drawing_area_size 1011 else: 1012 oss_w = self.drawing_area_size 1013 oss_h = self.drawing_area_size + ind_extra_s(self.indicator) 1014 1015 offscreen_surface = cairo.Surface.create_similar(self.app_surface, 1016 cairo.CONTENT_COLOR_ALPHA, 1017 oss_w, oss_h) 1018 1019 ctx = cairo.Context(offscreen_surface) 1020 1021 if self.is_dragee is False: 1022 # convert the highlight values to their cairo equivalents 1023 red = self.highlight_color.r / 255 1024 green = self.highlight_color.g / 255 1025 blue = self.highlight_color.b / 255 1026 1027 dbgd = None 1028 1029 if self.applet_win is not None: 1030 scale_factor = self.applet_win.get_scale_factor() 1031 else: 1032 scale_factor = 1 1033 1034 if self.active_bg == IconBgType.UNITY_FLAT: 1035 dbgd = UnityFlatBackgroundDrawer(ctx, self.drawing_area_size, 1036 self.applet_orient, red, green, blue, 1037 self.is_running(), scale_factor) 1038 elif self.active_bg == IconBgType.UNITY: 1039 dbgd = UnityBackgroundDrawer(ctx, self.drawing_area_size, 1040 self.applet_orient, red, green, blue, 1041 self.is_running(), scale_factor) 1042 elif self.is_active: 1043 if self.active_bg == IconBgType.GRADIENT: 1044 dbgd = DefaultBackgroundDrawer(ctx, self.drawing_area_size, 1045 self.applet_orient, red, green, blue) 1046 else: 1047 dbgd = AlphaFillBackgroundDrawer(ctx, self.drawing_area_size, 1048 self.applet_orient, red, green, blue, 0.5) 1049 1050 if dbgd is not None: 1051 dbgd.draw() 1052 1053 # draw the app icon 1054 if self.active_bg in [IconBgType.UNITY_FLAT, IconBgType.UNITY]: 1055 pb_size = (self.drawing_area_size) * 0.75 1056 offset = self.drawing_area_size / 2 - pb_size / 2 1057 ctx.set_source_surface(self.app_surface, offset, offset) 1058 else: 1059 ctx.set_source_surface(self.app_surface, 3, 3) 1060 1061 if self.is_pulsing: 1062 # draw the icon semi-transparently according to how far through the 1063 # animation we are 1064 1065 half_way = int(CONST_PULSE_STEPS / 2) 1066 if self.pulse_step <= half_way: 1067 alpha = 1.0 - (self.pulse_step / half_way) 1068 else: 1069 alpha = 0.0 + (self.pulse_step - half_way) / half_way 1070 1071 ctx.paint_with_alpha(alpha) 1072 1073 elif self.needs_attention and self.attention_type == dock_prefs.AttentionType.BLINK: 1074 if self.attention_blink_on: 1075 ctx.paint() # draw normally if in the flash on state 1076 elif self.is_dragee: 1077 ctx.paint_with_alpha(0.0) 1078 else: 1079 ctx.paint() 1080 ctx.save() 1081 if self.active_bg == IconBgType.UNITY_FLAT: 1082 if self.is_running(): 1083 dbgd.draw_shine() 1084 elif self.active_bg == IconBgType.UNITY: 1085 dbgd.draw_shine() 1086 ctx.restore() 1087 1088 if (self.has_mouse is True) and ((self.is_dragee is False) and (self.scroll_dir == ScrollType.SCROLL_NONE)): 1089 # lighten the icon 1090 ctx.set_operator(cairo.OPERATOR_ADD) 1091 ctx.paint_with_alpha(0.2) 1092 ctx.set_operator(cairo.OPERATOR_OVER) 1093 elif (self.has_mouse is True) and (self.scroll_dir != ScrollType.SCROLL_NONE): 1094 # this app indicates the dock can scroll, so we darken it 1095 ctx.set_operator(cairo.OPERATOR_DEST_OUT) 1096 ctx.paint_with_alpha(0.5) 1097 ctx.set_operator(cairo.OPERATOR_OVER) 1098 1099 # draw the app running indicators 1100 if (self.is_running()) and \ 1101 (self.indicator != IndicatorType.NONE) and \ 1102 (self.is_dragee is False): 1103 1104 # work out how many indicators to draw - either a single one or 1105 # one for each open window up to a maximum of 4, and take into 1106 # account the fact that we might only be showing indicators from 1107 # the current workspace 1108 1109 # get the number of indicators to show... 1110 if self.multi_ind is False and self.indicator != IndicatorType.SUBWAY: 1111 num_ind = 1 1112 else: 1113 num_ind = self.get_num_windows(self.ind_ws) 1114 if num_ind > 4: 1115 num_ind = 4 1116 1117 ind = None 1118 if self.indicator == IndicatorType.LIGHT: 1119 ind = DefaultLightInd(ctx, self.drawing_area_size, 1120 self.applet_orient, num_ind) 1121 elif self.indicator == IndicatorType.DARK: 1122 ind = DefaultDarkInd(ctx, self.drawing_area_size, 1123 self.applet_orient, num_ind) 1124 elif self.indicator == IndicatorType.TBAR: 1125 ind = ThemeBarInd(ctx, self.drawing_area_size, self.applet_orient, self.applet) 1126 elif self.indicator == IndicatorType.TCIRC: 1127 ind = ThemeCircleInd(ctx, self.drawing_area_size, self.applet_orient, self.applet, num_ind) 1128 elif self.indicator == IndicatorType.TSQUARE: 1129 ind = ThemeSquareInd(ctx, self.drawing_area_size, self.applet_orient, self.applet, num_ind) 1130 elif self.indicator == IndicatorType.TTRI: 1131 ind = ThemeTriInd(ctx, self.drawing_area_size, self.applet_orient, self.applet, num_ind) 1132 elif self.indicator == IndicatorType.TDIA: 1133 ind = ThemeDiaInd(ctx, self.drawing_area_size, self.applet_orient, self.applet, num_ind) 1134 elif self.indicator == IndicatorType.SUBWAY: 1135 ind = SubwayInd(ctx, self.drawing_area_size, self.applet_orient, 1136 self.applet, num_ind, offscreen_surface, self.is_active) 1137 1138 if ind is not None: 1139 ind.draw() 1140 1141 # do we need a count? 1142 if self.show_count: 1143 self.draw_count(ctx) 1144 1145 if self.show_progress: 1146 self.draw_progress(ctx) 1147 1148 if self.needs_attention and self.attention_type == dock_prefs.AttentionType.SHOW_BADGE: 1149 self.draw_attention_badge(ctx) 1150 1151 if not build_gtk2: 1152 # scrolling only available in GTK3 1153 if self.has_mouse and (self.scroll_dir != ScrollType.SCROLL_NONE): 1154 if self.scroll_dir == ScrollType.SCROLL_UP: 1155 self.draw_scroll_up(ctx) 1156 elif self.scroll_dir == ScrollType.SCROLL_DOWN: 1157 self.draw_scroll_down(ctx) 1158 1159 # now draw to the screen 1160 if build_gtk2: 1161 screen_ctx = self.drawing_area.window.cairo_create() 1162 screen_ctx.rectangle(event.area.x, event.area.y, 1163 event.area.width, event.area.height) 1164 screen_ctx.clip() 1165 1166 alloc = self.drawing_area.get_allocation() 1167 if (self.applet_orient == MatePanelApplet.AppletOrient.UP) or \ 1168 (self.applet_orient == MatePanelApplet.AppletOrient.DOWN): 1169 screen_ctx.set_source_surface(offscreen_surface, alloc.x, 0) 1170 else: 1171 screen_ctx.set_source_surface(offscreen_surface, 0, alloc.y) 1172 1173 screen_ctx.paint() 1174 screen_ctx = None 1175 else: 1176 event.set_source_surface(offscreen_surface, 0, 0) 1177 event.paint() 1178 alloc = self.drawing_area.get_allocation() 1179 1180 ctx = None 1181 1182 def draw_count(self, ctx): 1183 """ Draw the app's counter value 1184 1185 Args: ctx - the cairo context where the counter is to be drawn 1186 """ 1187 1188 # drawing is done at a notional size 64x64 px, and then scaled 1189 # appropriately according to self.drawing_area_size 1190 1191 draw_size = 64.0 1192 1193 # height of the counter = 2 pix border top and bottom + 16 pix 1194 # internal height 1195 height = 20 1196 1197 # work out the appropriate font size to use - has to fit within the 1198 # borders and provide some space above and below the count_val 1199 reqd_font_height = height - 8 1200 1201 # find a font size where the count can be shown with the required height 1202 ctx.select_font_face("", cairo.FONT_SLANT_NORMAL, cairo.FONT_WEIGHT_BOLD) 1203 ctext = "%d" % self.count_val 1204 for fsize in range(24, 2, -1): 1205 ctx.set_font_size(fsize) 1206 extents = ctx.text_extents(ctext) 1207 1208 if extents[3] < reqd_font_height: 1209 font_size = fsize 1210 break 1211 1212 # work out an appropriate width for the counter 1213 inset = height / 2 1214 radius = inset - 1 1215 if int(extents[2] + extents[0]) > int(radius): 1216 width = extents[2] + extents[0] + radius 1217 else: 1218 width = height + inset 1219 1220 ctx.save() 1221 1222 # the background color of the count is the app's highlight colour 1223 # convert the highlight values to their cairo equivalents 1224 bred = self.highlight_color.r / 255 1225 bgreen = self.highlight_color.g / 255 1226 bblue = self.highlight_color.b / 255 1227 1228 # set an appropriate text and border color 1229 if bred + bgreen + bblue > 1.5: # mid-level grey 1230 tred = tgreen = tblue = 0.0 1231 else: 1232 tred = tgreen = tblue = 1.0 1233 1234 # the count is placed in the upper right of the drawing area, and we need 1235 # to calculate it's position based on the notional DA_SIZE 1236 # 1237 1238 adj = 2 1239 left = draw_size - width + inset - adj 1240 1241 # do the drawing - attribution for the drawing code: 1242 # https://bazaar.launchpad.net/~unity-team/unity/trunk/view/head:/launcher/LauncherIcon.cpp 1243 ctx.scale(self.drawing_area_size / draw_size, 1244 self.drawing_area_size / draw_size) 1245 1246 ctx.move_to(left, height - 1 + adj) 1247 ctx.arc(left, inset + adj, radius, 0.5 * math.pi, 1.5 * math.pi) 1248 ctx.arc(draw_size - inset - adj, inset + adj, radius, 1.5 * math.pi, 0.5 * math.pi) 1249 ctx.line_to(left, height - 1 + adj) 1250 ctx.set_source_rgb(bred, bgreen, bblue) 1251 ctx.fill_preserve() 1252 ctx.set_source_rgb(tred, tgreen, tblue) 1253 ctx.set_line_width(2) 1254 ctx.stroke() 1255 1256 # draw the text 1257 ctx.move_to(left - inset + width / 2 - (extents[0] + extents[2] / 2), 1258 (height / 2) + adj + extents[3] / 2) 1259 ctx.set_source_rgb(tred, tgreen, tblue) 1260 ctx.show_text(ctext) 1261 1262 ctx.restore() 1263 1264 def draw_progress(self, ctx): 1265 """ Draw a progress bar to show the app's progress value 1266 1267 Args: ctx - the cairo context where the counter is to be drawn 1268 """ 1269 1270 def rounded_rectangle(cr, x, y, w, h, r=20): 1271 """ Convenience function to draw a rounded rectangle 1272 # Attribution: 1273 # https://stackoverflow.com/questions/2384374/rounded-rectangle-in-pygtk 1274 # This is just one of the samples from 1275 # http://www.cairographics.org/cookbook/roundedrectangles/ 1276 # A****BQ 1277 # H C 1278 # * * 1279 # G D 1280 # F****E 1281 """ 1282 1283 cr.move_to(x + r, y) # Move to A 1284 cr.line_to(x + w - r, y) # Straight line to B 1285 cr.curve_to(x + w, y, x + w, y, x + w, y + r) 1286 # Curve to C, Control points are both at Q 1287 cr.line_to(x + w, y + h - r) # Move to D 1288 cr.curve_to(x + w, y + h, x + w, y + h, x + w - r, y + h) # Curve to E 1289 cr.line_to(x + r, y + h) # Line to F 1290 cr.curve_to(x, y + h, x, y + h, x, y + h - r) # Curve to G 1291 cr.line_to(x, y + r) # Line to H 1292 cr.curve_to(x, y, x, y, x + r, y) # Curve to A 1293 1294 # drawing is done on a to scale of 64x64 pixels and then scaled 1295 # down to the fit the app's drawing area 1296 draw_size = 64.0 1297 1298 # the foreground colour of the progress is the app's highlight colour 1299 # convert the highlight values to their cairo equivalents 1300 fred = self.highlight_color.r / 255 1301 fgreen = self.highlight_color.g / 255 1302 fblue = self.highlight_color.b / 255 1303 1304 # set an appropriate border color and also a background colour for 1305 # the progress bar, based on the highlight colour 1306 if fred + fgreen + fblue > 1.5: # mid-level grey 1307 brd_red = brd_green = brd_blue = 0.0 1308 bk_red = bk_green = bk_blue = 1.0 1309 else: 1310 brd_red = brd_green = brd_blue = 1.0 1311 bk_red = bk_green = bk_blue = 0.0 1312 1313 height = 8 # total height of the progress bar 1314 line_width = 2 # border line width 1315 int_height = height - line_width * 2 # interior height 1316 left = 8.5 1317 width = draw_size - left * 2 # width of the progress bar 1318 1319 top = (draw_size / 8) * 5 + 0.5 1320 1321 ctx.save() 1322 ctx.scale(self.drawing_area_size / draw_size, self.drawing_area_size / draw_size) 1323 1324 # fill the interior with the background colour 1325 ctx.set_line_width(1) 1326 ctx.set_source_rgb(bk_red, bk_green, bk_blue) 1327 rounded_rectangle(ctx, left, top, width, height, 7) 1328 ctx.stroke_preserve() 1329 ctx.fill() 1330 1331 # fill part of the interior with a different colour, depending on 1332 # the progress value 1333 ctx.set_source_rgb(fred, fgreen, fblue) 1334 rounded_rectangle(ctx, left + line_width - 1, top, 1335 (width - (line_width - 1) * 2) * self.progress_val, 1336 height, 7) 1337 ctx.fill() 1338 1339 # draw exterior of the progress bar 1340 ctx.set_source_rgb(brd_red, brd_green, brd_blue) 1341 ctx.set_line_width(2) 1342 rounded_rectangle(ctx, left, top, width, height, 7) 1343 ctx.stroke() 1344 1345 ctx.restore() 1346 1347 def draw_attention_badge(self, ctx): 1348 """ Draw a badge on the app icon to indicate the app requires 1349 attention 1350 1351 Basically a copy and paste of draw_count... 1352 1353 Args: ctx - the cairo context where the counter is to be drawn 1354 """ 1355 1356 # drawing is done at a notional size 64x64 px, and then scaled 1357 # appropriately according to self.drawing_area_size 1358 1359 draw_size = 64.0 1360 1361 # height of the exaclamation mark = 2 pix border top and bottom + 16 pix 1362 # internal height 1363 height = 20 1364 1365 # work out the appropriate font size to use - has to fit within the 1366 # borders and provide some space above and below the exclamation mark 1367 reqd_font_height = height - 8 1368 1369 # find a font size where the count can be shown with the required height 1370 ctx.select_font_face("", cairo.FONT_SLANT_NORMAL, cairo.FONT_WEIGHT_BOLD) 1371 ctext = "!" 1372 for fsize in range(24, 2, -1): 1373 ctx.set_font_size(fsize) 1374 extents = ctx.text_extents(ctext) 1375 1376 if extents[3] < reqd_font_height: 1377 font_size = fsize 1378 break 1379 1380 # work out an appropriate width for the badge 1381 inset = height / 2 1382 radius = inset - 1 1383 if int(extents[2] + extents[0]) > int(radius): 1384 width = extents[2] + extents[0] + radius 1385 else: 1386 width = height 1387 # width = height + inset 1388 1389 width = extents[2] + extents[0] + radius 1390 ctx.save() 1391 1392 # the background color of the badge is the app's highlight colour 1393 # convert the highlight values to their cairo equivalents 1394 bred = self.highlight_color.r / 255 1395 bgreen = self.highlight_color.g / 255 1396 bblue = self.highlight_color.b / 255 1397 1398 # set an appropriate text and border color 1399 if bred + bgreen + bblue > 1.5: # mid-level grey 1400 tred = tgreen = tblue = 0.0 1401 else: 1402 tred = tgreen = tblue = 1.0 1403 1404 # the badge is placed in the upper left of the drawing area 1405 adj = 2 1406 left = inset 1407 1408 # do the drawing - attribution for the drawing code: 1409 # https://bazaar.launchpad.net/~unity-team/unity/trunk/view/head:/launcher/LauncherIcon.cpp 1410 ctx.scale(self.drawing_area_size / draw_size, 1411 self.drawing_area_size / draw_size) 1412 1413 ctx.move_to(left, height - 1 + adj) 1414 ctx.arc(left, inset + adj, radius, 0.5 * math.pi, 1.5 * math.pi) 1415 ctx.arc(left + width - inset - adj, inset + adj, radius, 1.5 * math.pi, 0.5 * math.pi) 1416 ctx.line_to(left, height - 1 + adj) 1417 ctx.set_source_rgb(bred, bgreen, bblue) 1418 ctx.fill_preserve() 1419 ctx.set_source_rgb(tred, tgreen, tblue) 1420 ctx.set_line_width(2) 1421 ctx.stroke() 1422 1423 # draw the text 1424 ctx.move_to(left + inset - (extents[0] + extents[2] / 2) - radius, 1425 (height / 2) + adj + extents[3] / 2) 1426 ctx.set_source_rgb(tred, tgreen, tblue) 1427 ctx.show_text(ctext) 1428 1429 ctx.restore() 1430 1431 def draw_scroll_up(self, ctx): 1432 """ To indicate that the docked app can scroll up (or left on horizontal panels) 1433 draw an up (or left) arrow on the icon 1434 1435 Params : 1436 context : the docked app's cairo context for us to draw on 1437 size : the size of the context, in pixels 1438 orient : the orientation of the dock applet 1439 """ 1440 1441 if self.drawing_area_size > 48: 1442 icon_size = Gtk.IconSize.DND 1443 icon_pix = 24 1444 else: 1445 icon_size = Gtk.IconSize.LARGE_TOOLBAR 1446 icon_pix = 16 1447 1448 if self.applet_orient in [MatePanelApplet.AppletOrient.UP, 1449 MatePanelApplet.AppletOrient.DOWN]: 1450 1451 arrow_pb = self.drawing_area.render_icon(Gtk.STOCK_GO_BACK, icon_size, None) 1452 arrow_pb = arrow_pb.scale_simple(self.drawing_area_size / 2, self.drawing_area_size / 2, 1453 GdkPixbuf.InterpType.BILINEAR) 1454 1455 Gdk.cairo_set_source_pixbuf(ctx, arrow_pb, 0, self.drawing_area_size / 4) 1456 1457 else: 1458 arrow_pb = self.drawing_area.render_icon(Gtk.STOCK_GO_UP, icon_size, None) 1459 arrow_pb = arrow_pb.scale_simple(self.drawing_area_size / 2, self.drawing_area_size / 2, 1460 GdkPixbuf.InterpType.BILINEAR) 1461 1462 Gdk.cairo_set_source_pixbuf(ctx, arrow_pb, self.drawing_area_size / 4, 0) 1463 1464 ctx.paint() 1465 1466 def draw_scroll_down(self, ctx): 1467 """ To indicate that the docked app can scroll up (or left on horizontal panels) 1468 draw an up (or left) arrow on the icon 1469 1470 Params : 1471 context : the docked app's cairo context for us to draw on 1472 size : the size of the context, in pixels 1473 orient : the orientation of the dock applet 1474 """ 1475 1476 if self.drawing_area_size > 48: 1477 icon_size = Gtk.IconSize.DND 1478 icon_pix = 24 1479 else: 1480 icon_size = Gtk.IconSize.LARGE_TOOLBAR 1481 icon_pix = 16 1482 1483 if self.applet_orient in [MatePanelApplet.AppletOrient.UP, 1484 MatePanelApplet.AppletOrient.DOWN]: 1485 1486 arrow_pb = self.drawing_area.render_icon(Gtk.STOCK_GO_FORWARD, icon_size, None) 1487 arrow_pb = arrow_pb.scale_simple(self.drawing_area_size / 2, self.drawing_area_size / 2, 1488 GdkPixbuf.InterpType.BILINEAR) 1489 1490 Gdk.cairo_set_source_pixbuf(ctx, arrow_pb, self.drawing_area_size / 2, 1491 self.drawing_area_size / 4) 1492 1493 else: 1494 arrow_pb = self.drawing_area.render_icon(Gtk.STOCK_GO_DOWN, icon_size, None) 1495 arrow_pb = arrow_pb.scale_simple(self.drawing_area_size / 2, self.drawing_area_size / 2, 1496 GdkPixbuf.InterpType.BILINEAR) 1497 1498 Gdk.cairo_set_source_pixbuf(ctx, arrow_pb, self.drawing_area_size / 4, self.drawing_area_size / 2) 1499 1500 ctx.paint() 1501 1502 def set_pixbuf(self, pixbuf): 1503 """Set the app pixbuf and calculate its average colour. 1504 """ 1505 1506 self.app_pb = pixbuf 1507 1508 rht, ght, bht = self.highlight_color = get_backlight_color(pixbuf) 1509 self.highlight_color = ColorTup(r=rht, g=ght, b=bht) 1510 1511 def set_surface(self, surface): 1512 """Set the app surface 1513 """ 1514 1515 self.app_surface = surface 1516 1517 def start_app(self): 1518 """Start the app or open a new window if it's already running 1519 1520 Use Gio.DesktopAppinfo as it supports startup notfication 1521 """ 1522 # start the app 1523 try: 1524 run_it = self.desktop_ai.get_string("Exec") 1525 except: 1526 run_it = None 1527 1528 if run_it is not None: 1529 1530 # hack for Linux Mint: 1531 # Mint has several shortcuts for starting caja so that it can 1532 # be started in a specific directory e.g. home, /, etc 1533 # However, the main caja.desktop is responsible for starting the 1534 # user's desktop and this is the .desktop file the applet finds 1535 # first. 1536 # When the caja icon on the applet is clicked, caja is run as a 1537 # desktop window and no new file browser appears. 1538 # To get around this, we can simply check the command that is going 1539 # to be run and change it so that a caja window opens in the user's 1540 # home directory, which is the behaviour they'll probably be 1541 # expecting.... 1542 if run_it == "/usr/bin/startcaja": 1543 run_it = "caja" 1544 self.run_cmd_line(run_it) 1545 return 1546 1547 if self.desktop_file is not None: 1548 gdai = Gio.DesktopAppInfo.new_from_filename(self.desktop_file) 1549 else: 1550 gdai = None 1551 1552 disp = Gdk.Display.get_default() 1553 if build_gtk2: 1554 alc = Gdk.AppLaunchContext() 1555 else: 1556 alc = disp.get_app_launch_context() 1557 1558 alc.set_desktop(-1) # use default screen & desktop 1559 alc.set_timestamp(Gtk.get_current_event_time()) 1560 alc.connect("launch-failed", self.launch_failed) 1561 1562 # indicate we want startup notification 1563 if gdai is not None: 1564 self.startup_id = alc.get_startup_notify_id(gdai, []) 1565 1566 gdai.launch_uris_as_manager([], alc, GLib.SpawnFlags.SEARCH_PATH, 1567 None, None, None, None) 1568 1569 # make the app's icon pulse 1570 if not self.is_running(): 1571 throbber = PulseTimer(self) 1572 else: 1573 # if the app is already running, we want the icon to pulse at most once only 1574 # For apps which don't open a new window, the pulse timer will end up cancelling 1575 # the unneeded startup notification 1576 throbber = PulseTimer(self, True) 1577 1578 def cancel_startup_notification(self): 1579 """ 1580 Cancel any startup notification 1581 """ 1582 1583 if build_gtk2: 1584 Gdk.notify_startup_complete_with_id(self.startup_id) 1585 else: 1586 display = Gdk.Display.get_default() 1587 display.notify_startup_complete(self.startup_id) 1588 1589 self.startup_id = None 1590 1591 def launch_failed(self, app_launch_context, startup_id): 1592 """Handler for app launch failure events 1593 1594 Cancel the startup notification 1595 1596 Args: 1597 app_launch_context : the Gdk.AppLaunchContext that failed 1598 startup_id : the startup notification id 1599 """ 1600 1601 self.cancel_startup_notification() 1602 1603 display = Gdk.Display.get_default() 1604 display.notify_startup_complete(startup_id) 1605 1606 def run_cmd_line(self, cmd_line): 1607 """Run a command line. 1608 1609 Args: 1610 cmd_line - the command to run 1611 """ 1612 1613 # TODO: this is old code and needs to be removed 1614 1615 # the command line may contain escape sequences, so unescape them.... 1616 cmd_line = bytearray(cmd_line, "UTF-8") 1617 cmd_line = cmd_line.decode("unicode-escape") 1618 1619 # if an environment variable is specified, extract its name an value 1620 # Note: the .desktop file specification at 1621 # https://specifications.freedesktop.org/desktop-entry-spec/latest/ar01s06.html 1622 # does not mention this. Both Ubuntu 1623 # https://help.ubuntu.com/community/EnvironmentVariables#Launching_desktop_application_with_an_environment_variable 1624 # and Arch linux 1625 # https://wiki.archlinux.org/index.php/Desktop_entries#Modify_environment_variables 1626 # seem to indicate that only a single variable can be set and that 1627 # there are no spaces between the variable name, the '=' character and 1628 # variable's value ..... 1629 1630 # so, if cmd_line begins with "env" it specifies an environment variable 1631 # to set, follwed by the app e.g. env LANG=he_IL.UTF-8 /usr/bin/pluma 1632 # 1633 if cmd_line.startswith("env"): 1634 cmd_parts = cmd_line.split(" ", 2) 1635 var_parts = cmd_parts[1].split("=") 1636 var_name = var_parts[0] 1637 var_value = var_parts[1] 1638 1639 # now we need to get the app path and args and carry on... 1640 cmd_line = cmd_parts[2] 1641 else: 1642 var_name = None 1643 var_value = None 1644 1645 # if any of the directories in cmd_line contain a " ", they need to be 1646 # escaped 1647 head, tail = os.path.split(cmd_line) 1648 if " " in head: 1649 head = head.replace(" ", "\\ ") 1650 cmd_line = head + "/" + tail 1651 app_info = Gio.AppInfo.create_from_commandline(cmd_line, 1652 None, 1653 Gio.AppInfoCreateFlags.SUPPORTS_STARTUP_NOTIFICATION) 1654 alc = Gdk.AppLaunchContext() 1655 alc.set_desktop(-1) # use default screen & desktop 1656 alc.set_timestamp(Gtk.get_current_event_time()) 1657 1658 # if the .desktop specfied an environment variable, set it 1659 if (var_name is not None) and (var_value is not None): 1660 alc.setenv(var_name, var_value) 1661 1662 file_list = GLib.List() 1663 1664 # app_info.launch(None, alc) 1665 self.startup_id = alc.get_startup_notify_id(app_info, []) 1666 1667 throbber = PulseTimer(self) 1668 1669 def run_rc_action(self, act_no): 1670 """ run the right click action specified by act_no 1671 1672 Args: 1673 act_no - integer, the action number to run 1674 """ 1675 1676 if len(self.rc_actions) >= act_no: 1677 if build_gtk2: 1678 alc = Gdk.AppLaunchContext() 1679 else: 1680 disp = Gdk.Display.get_default() 1681 alc = disp.get_app_launch_context() 1682 1683 alc.set_desktop(-1) # use default screen & desktop 1684 alc.set_timestamp(Gtk.get_current_event_time()) 1685 alc.connect("launch-failed", self.launch_failed) 1686 1687 # indicate we want startup notification 1688 self.startup_id = alc.get_startup_notify_id(self.desktop_ai, []) 1689 1690 self.desktop_ai.launch_action(self.rc_actions[act_no - 1], alc) 1691 self.start_pulsing() 1692 1693 def get_rc_action(self, act_no): 1694 """ return a specified right click action's details 1695 1696 Args: 1697 act_no - integer, the specified action number 1698 1699 Returns: 1700 bool - True if the action exists, False otherwise 1701 string - the name of the action (i.e. the text to appear in the 1702 right click menu) 1703 """ 1704 1705 if len(self.rc_actions) >= act_no: 1706 return True, self.desktop_ai.get_action_name(self.rc_actions[act_no - 1]) 1707 else: 1708 return False, "" 1709 1710 def start_pulsing(self): 1711 """ start the dock icon pulsing 1712 """ 1713 1714 throbber = PulseTimer(self) 1715 1716 def pulse_once(self): 1717 """ Make the dock icon pulse once""" 1718 1719 throbber = PulseTimer(self, True) 1720 1721 def set_dragee(self, is_dragee): 1722 """ Set the flag which indicates whether or not this app is being 1723 dragged to a new position on the dock 1724 1725 Set the value of the self.is_dragee flag and redraw the app icon 1726 """ 1727 1728 self.is_dragee = is_dragee 1729 self.queue_draw() 1730 1731 def set_progress_visible(self, is_visible): 1732 """ 1733 Update the progress visibility and cause the app's icon to be 1734 redrawn 1735 1736 Args: 1737 is_visible : whether the progress is to be displayed 1738 """ 1739 1740 if is_visible != self.show_progress: 1741 self.show_progress = bool(is_visible) 1742 self.queue_draw() 1743 1744 def set_progress_value(self, val): 1745 """ 1746 Update the progress value and cause the app's icon to be 1747 redrawn 1748 1749 Args: 1750 val : the counter value 1751 """ 1752 1753 # if the new progressvalue is the same as the old, then there's no need 1754 # to do anything... 1755 if val != self.progress_val: 1756 self.progress_val = val 1757 self.queue_draw() 1758 1759 def set_counter_visible(self, is_visible): 1760 """ 1761 Update the counter visibility and cause the app's icon to be 1762 redrawn 1763 1764 Args: 1765 is_visible : whether the counter is to be displayed 1766 """ 1767 1768 # if the new value is the same as the old, then there's no need 1769 # to do anything... 1770 if is_visible != self.show_count: 1771 self.show_count = bool(is_visible) 1772 self.queue_draw() 1773 1774 def set_counter_value(self, val): 1775 """ 1776 Update the counter value and cause the app's icon to be 1777 redrawn 1778 1779 Args: 1780 val : the counter value 1781 """ 1782 1783 # if the new counter value is the same as the old, then there's no need 1784 # to do anything... 1785 if val != self.count_val: 1786 self.count_val = val 1787 self.queue_draw() 1788 1789 def set_scroll_dir(self, scroll_dir): 1790 """ 1791 Sets the app's scroll direction 1792 1793 Param: scroll_dir - a docked_app_helpers.ScrollType 1794 """ 1795 1796 self.scroll_dir = scroll_dir 1797 1798 1799def main(): 1800 """Main function. 1801 1802 Debugging code can go here 1803 """ 1804 pass 1805 1806 1807if __name__ == "__main__": 1808 main() 1809