Changeset 187

Show
Ignore:
Timestamp:
03/21/2006 09:51:06 AM (3 years ago)
Author:
petli
Message:

Rewrote yesterdays hack to get dynamic border colors. Now it is much more flexible, implemented as a client filter map to border color managers, allowing fine-tuned control of which clients get which colors.

Location:
trunk
Files:
3 modified

Legend:

Unmodified
Added
Removed
  • trunk/NEWS

    r186 r187  
    99respectively.  
    1010 
    11 ** New border mixin: DynamicColorBorderClient 
    12  
    13 This mixin chooses the border color dynamically based on the window 
    14 title.  Will only work well on non-palette visuals, on PseudoColor 
    15 displays and the like it will soon have gobbled all colors. 
     11** border.BorderClient rewritten 
     12 
     13The ancient BorderClient mixin has been rewritten to allow more 
     14dynamic color choosing schemes.  It should be backward-compatible, but 
     15for those willing to change their wm scripts it is now possible to 
     16choose different colors for different windows. 
     17 
     18This is handled by letting client filters map to a certain 
     19BorderColorManager subclass object.  FixedBorderColor gives clients a 
     20static color, while TitleBorderColor chooses a color based on the 
     21window title.  This is useful to distinguish different xterm windows, 
     22if e.g. the current directory is included in the window title. 
     23 
     24It should be easy to define your own color managers, to get even more 
     25dynamic colors. 
     26 
     27For details, run "pydoc plwm.border". 
    1628 
    1729 
  • trunk/examples/petliwm.py

    r186 r187  
    173173class MyClient(wmanager.Client, 
    174174               outline.XorOutlineClient, 
    175                border.DynamicColorBorderClient, 
     175               border.BorderClient, 
    176176               modestatus.ModeFocusedTitleClient, 
    177177               misc.InitialKeepOnScreenClient, 
     
    187187                           'XTerm': (-1, 0)} 
    188188 
    189     border_color_name = "grey20" 
    190     border_focuscolor_name = "grey60" 
    191  
    192     border_dynamic_saturation = 0.3 
    193     border_dynamic_brightness = 0.7 
    194  
    195     border_dynamic_focus_saturation = 1.0 
    196     border_dynamic_focus_brightness = 0.7 
     189    # Use title-based border colors for xterms, but fixed greys for 
     190    # all other windows 
     191    border_colors = [(name('XTerm'), border.TitleBorderColor(0.3, 0.7, 1.0, 0.7)) 
     192                     ] 
     193    border_default_color = border.FixedBorderColor('grey20', 'grey60') 
    197194 
    198195    traceim_filters = [ 
  • trunk/plwm/border.py

    r186 r187  
    1 # $Id: border.py,v 1.8 2006-03-20 18:16:17 petli Exp $ 
     1# $Id: border.py,v 1.9 2006-03-21 15:51:06 petli Exp $ 
    22# 
    33# border.py -- change border color on focused client 
    44# 
    5 #    Copyright (C) 1999-2001  Peter Liljenberg <petli@ctrl-c.liu.se> 
     5#    Copyright (C) 1999-2001,2006  Peter Liljenberg <petli@ctrl-c.liu.se> 
    66# 
    77#    This program is free software; you can redistribute it and/or modify 
     
    2222import wmanager, wmevents, cfilter 
    2323 
    24 class BorderClientBase: 
     24class BorderClient: 
     25    """Client mixin class managing a simple window border. 
     26 
     27    The border has a width defined by the attribute 
     28    border_default_width.  The attribute no_border_clients is a client 
     29    filter that causes matching clients to have no border at all. 
     30 
     31    The color of the border is managed by objects of some 
     32    BorderColorManager subclass.  border_colors is a list of tuples 
     33    (filter, manager).  When a new client is created the list is 
     34    processed from the beginning and clients matching filter will have 
     35    their border colors managed by the manager object.  If no filter 
     36    matches the manager border_default_color is used instead. 
     37    """ 
     38     
    2539    no_border_clients = cfilter.false 
    2640    border_default_width = 3 
    2741 
     42    border_colors = () 
     43    border_default_color = None 
     44 
     45    # These two attributes are for backward compitability, and is 
     46    # equivalent to this: 
     47    # border_default_color = FixedBorderColor(border_color_name, 
     48    #                                         border_focuscolor_name) 
    2849    border_color_name = None 
    2950    border_focuscolor_name = None 
     
    3960            self.setborderwidth(self.border_default_width) 
    4061 
     62            manager = None 
     63            for f, m in self.border_colors: 
     64                if f(self): 
     65                    manager = m 
     66                    break 
     67            else: 
     68                manager = self.border_default_color 
     69 
     70            if manager is None: 
     71                # Use old color attributes 
     72                manager = FixedBorderColor(self.border_color_name, 
     73                                           self.border_focuscolor_name) 
     74 
     75            manager.set_client_border_colors(self) 
     76 
     77            # Set up dispatchers for the colors 
    4178            self.dispatch.add_handler(wmevents.ClientFocusIn, self.border_get_focus) 
    4279            self.dispatch.add_handler(wmevents.ClientFocusOut, self.border_lose_focus) 
    4380 
    44             self.border_set_colors() 
    45             self.border_update() 
    46  
    47     def border_update(self): 
    48         """Update the border color.  Call this after chaning 
    49         border_color or border_focuscolor 
    50         """ 
     81 
     82    def border_set_colors(self, blurred, focused): 
     83        """This method should be called by the BorderColor object to 
     84        set the border colors of the client, but might be called by 
     85        other mixins too 
     86 
     87        blurred and focused are the pixel values to be used for 
     88        non-focused and focused clients, respectively. 
     89        """ 
     90 
     91        self.border_color = blurred 
     92        self.border_focuscolor = focused 
     93         
    5194        if self.focused: 
    5295            if self.border_focuscolor is not None: 
     
    5699                self.window.change_attributes(border_pixel = self.border_color)  
    57100 
    58     def border_set_colors(self): 
    59         """Override this to set border_color and border_focuscolor. 
    60         This is called during client initialization to get the initial colors. 
    61         """ 
    62         raise NotImplementedError('%s.border_set_colors()' % self.__class__) 
    63              
     101    # 
     102    # Event handlers 
     103    # 
     104     
    64105    def border_get_focus(self, event): 
    65106        if self.border_focuscolor is not None: 
     
    71112 
    72113 
    73 class BorderClient(BorderClientBase): 
    74     """Set border color to one or another if the client is focused or not.""" 
    75     border_color_name = None 
    76     border_focuscolor_name = None 
    77  
    78     def border_set_colors(self): 
    79         resname = self.wm.rdb_get('.border.color', '.Border.Color', '#000000') 
    80         if self.border_color_name is not None: 
    81             self.border_color = self.screen.get_color( 
    82                 self.border_color_name, default = resname) 
    83         else: 
    84             self.border_color = self.screen.get_color(resname) 
    85  
    86         resname = self.wm.rdb_get('.border.focus.color', 
    87                                   '.Border.Focus.Color', '#000000') 
    88         if self.border_focuscolor_name is not None: 
    89             self.border_focuscolor = self.screen.get_color( 
    90                 self.border_focuscolor_name, default = resname) 
    91         else: 
    92             self.border_focuscolor = self.screen.get_color(resname) 
    93  
    94  
    95 class DynamicColorBorderClient(BorderClientBase): 
    96  
    97     """Change the color of the border based on some client property, 
    98     by default the window title. 
    99  
    100     Obviously this class shouldn't be used with palette-based visuals. 
    101  
    102  
    103     Subclass and override border_dynamic_install_handler() and 
    104     border_dynamic_get_hue() to change this behaviour. 
    105  
    106     The hue will be the same for a client (as long as the title or 
    107     whatever doesn't change) but the saturation and brightness is 
    108     changed depending on the client is focused or not.  This is 
    109     controlled with four attributes, in the range [0.0, 1.0]: 
    110  
    111     Non-focused: 
    112       border_dynamic_saturation  
    113       border_dynamic_brightness 
    114  
    115     Focused: 
    116       border_dynamic_focus_saturation  
    117       border_dynamic_focus_brightness 
    118  
    119  
     114class BorderColorManager: 
     115    """Base class for border color manager objects. 
     116 
     117    It should be subclassed by actual managers, which must implement 
     118    the set_client_border_colors() method. 
     119    """ 
     120 
     121    def set_client_border_colors(self, client): 
     122        """Override this to implement the border color selection for client. 
     123        """ 
     124        raise NotImplementedError('%s.set_client_border_colors()' % self.__class__) 
     125 
     126 
     127class FixedBorderColor(BorderColorManager): 
     128    """Use fixed border colors, one for non-focused and one for focused clients. 
     129    """ 
     130 
     131    def __init__(self, blurred, focused): 
     132        """Set the color of all clients to blurred and focused, resp. 
     133 
     134        They can either be strings naming a color, or three-tuples 
     135        specifying a color as an (r, g, b) value in the range [0, 65535]. 
     136        """ 
     137        self.blurred_color = blurred 
     138        self.focused_color = focused 
     139 
     140    def set_client_border_colors(self, client): 
     141        # The rdb stuff should be removed, but maybe someone is still using it... 
     142 
     143        resname = client.wm.rdb_get('.border.color', '.Border.Color', '#000000') 
     144        if self.blurred_color is not None: 
     145            blurred = client.screen.get_color(self.blurred_color, default = resname) 
     146        else: 
     147            blurred = client.screen.get_color(resname) 
     148 
     149        resname = client.wm.rdb_get('.border.focus.color', 
     150                                    '.Border.Focus.Color', '#000000') 
     151        if self.focused_color is not None: 
     152            focused = client.screen.get_color(self.focused_color, default = resname) 
     153        else: 
     154            focused = client.screen.get_color(resname) 
     155 
     156        client.border_set_colors(blurred, focused) 
     157 
     158 
     159class TitleBorderColor(BorderColorManager): 
     160    """Change the color of the border based on the client title. 
     161 
     162    The hue will be the same for a client (as long as the title 
     163    doesn't change) but the saturation and brightness is changed 
     164    depending on the client is focused or not, using an HSV-to-RGB translation. 
     165     
    120166    However, experimentation shows that HSV isn't very good, as 
    121167    confirmed by 
    122168    http://www.poynton.com/notes/colour_and_gamma/ColorFAQ.html#RTFToC36 
    123169 
    124     Varying the brightness changes the perceived color, only changing 
    125     the saturation works reasonably well.  This might be a problem 
    126     with the colorsys module, too. 
     170    Varying the brightness changes the perceived color, only just 
     171    changing the saturation works reasonably well.  This might be a 
     172    problem with the colorsys module, too. 
    127173 
    128174    If anyone manages to understand it, maybe the CIE XYZ model should 
     
    130176    """ 
    131177 
    132     border_dynamic_saturation = 0.3 
    133     border_dynamic_brightness = 0.7 
    134  
    135     border_dynamic_focus_saturation = 1.0 
    136     border_dynamic_focus_brightness = 0.7 
    137  
    138     def __client_init__(self): 
    139         # FIXME: check visual, and scream if it isn't suitable for 
    140         # this kind of color-grabbing.  Even better: fall back on 
    141         # default colors. 
    142          
    143         BorderClientBase.__client_init__(self) 
    144         self.border_dynamic_install_handler() 
    145  
    146     # 
    147     # Override these methods to change color property 
    148     # 
    149      
    150     def border_dynamic_install_handler(self): 
    151         self.dispatch.add_handler(X.PropertyNotify, self.border_dynamic_property_notify) 
    152  
    153     def border_dynamic_get_hue(self): 
     178    class TitleUpdater: 
     179        def __init__(self, manager, client): 
     180            self.manager = manager 
     181            self.client = client 
     182 
     183        def __call__(self, event): 
     184            if event.atom == Xatom.WM_NAME: 
     185                self.manager.choose_color(self.client) 
     186             
     187    def __init__(self, blurred_saturation = 0.3, 
     188                 blurred_brightness = 0.7, 
     189                 focused_saturation = 1.0, 
     190                 focused_brightness = 0.7): 
     191 
     192        """The saturation and brightness values should all be in the 
     193        range [0.0, 1.0]. 
     194 
     195        As noted above, the two brightness values should typically be 
     196        the same, otherwise the color will be perceived to change in 
     197        hue when the window is focused. 
     198        """ 
     199 
     200        self.blurred_saturation = blurred_saturation 
     201        self.blurred_brightness = blurred_brightness 
     202        self.focused_saturation = focused_saturation 
     203        self.focused_brightness = focused_brightness 
     204 
     205    def set_client_border_colors(self, client): 
     206        # Add a dispatcher that updates the colors when the title changes 
     207        client.dispatch.add_handler(X.PropertyNotify, self.TitleUpdater(self, client)) 
     208 
     209        # And set the initial colors 
     210        self.choose_color(client) 
     211 
     212    def choose_color(self, client): 
     213 
     214        # FIXME: release old colors first to work well in PseudoColor 
     215        # visuals.  Would require extensions to the color module, 
     216        # though, reference counting and so on, and also an event 
     217        # handler to free colors when the client is closed. 
     218         
     219        hue = self.get_hue(client) 
     220 
     221        blurred = self.get_color_hsv(client, hue, 
     222                                     self.blurred_saturation, 
     223                                     self.blurred_brightness) 
     224 
     225        focused = self.get_color_hsv(client, hue, 
     226                                     self.focused_saturation, 
     227                                     self.focused_brightness) 
     228 
     229        client.border_set_colors(blurred, focused) 
     230         
     231 
     232    def get_hue(self, client): 
    154233        """Return the hue for this client, in the range [0.0, 1.0]""" 
    155234 
     
    157236        # at least 24 bits.  Xor them into eight bits and use that for hue 
    158237 
    159         th = hash(self.get_title()) 
     238        th = hash(client.get_title()) 
    160239 
    161240        h = th & 0xff 
     
    165244        return float(h) / 0xff 
    166245 
    167     # 
    168     # Internalish methods 
    169     # 
    170  
    171     def border_dynamic_property_notify(self, event): 
    172         if event.atom == Xatom.WM_NAME: 
    173             self.border_set_colors() 
    174             self.border_update() 
    175  
    176     def border_set_colors(self): 
    177         hue = self.border_dynamic_get_hue() 
    178  
    179         self.border_color = self.border_dynamic_get_color_hsv( 
    180             hue, self.border_dynamic_saturation, 
    181             self.border_dynamic_brightness) 
    182  
    183         self.border_focuscolor = self.border_dynamic_get_color_hsv( 
    184             hue, self.border_dynamic_focus_saturation, 
    185             self.border_dynamic_focus_brightness) 
    186  
    187          
    188     def border_dynamic_get_color_hsv(self, hue, saturation, brightness): 
     246         
     247    def get_color_hsv(self, client, hue, saturation, brightness): 
    189248        import colorsys 
    190249        r, g, b = colorsys.hsv_to_rgb(hue, saturation, brightness) 
    191         return self.screen.get_color((r * 65535, g * 65535, b * 65535)) 
    192  
    193     # 
    194     # I tested using chroma instead of HSV.  Didn't really work that 
    195     # much better, but I keep the code here if anyone want's to experiment with it 
    196     # 
    197      
    198     border_dynamic_luminance = 0.3 
    199     border_dynamic_focus_luminance = 0.7 
    200  
    201     def border_dynamic_get_chroma(self): 
    202         """Return the Cr, Cb for this client, in the range [0, 255]""" 
    203  
    204         h = hash(self.get_title()) 
    205         return h & 0xff, (h >> 8) & 0xff 
    206  
    207     def border_set_colors_chroma(self): 
    208         cr, cb = self.border_dynamic_get_chroma() 
    209  
    210         self.border_color = self.border_dynamic_get_color( 
    211             self.border_dynamic_luminance, cr, cb) 
    212         self.border_focuscolor = self.border_dynamic_get_color( 
    213             self.border_dynamic_focus_luminance, cr, cb) 
    214  
    215     def border_dynamic_get_chroma_color(self, y, cb, cr): 
    216         # See http://www.poynton.com/notes/colour_and_gamma/ColorFAQ.html#RTFToC30 
    217  
    218         # scale [0.0, 1.0] to [16, 235] => [0, 219] 
    219         y *= 219 
    220  
    221         # get cr and cb in range [-112, +112] 
    222         cb = max(min(cb - 128, 112), -112) 
    223         cr = max(min(cr - 128, 112), -112) 
    224  
    225         # Do the matrix multiplication, but skip the divide-by-256 as 
    226         # we actually want the range [0, 65535] 
    227          
    228         r = (298.082 * y + 408.583 * cr) 
    229         g = (298.082 * y - 100.291 * cb - 208.120 * cr) 
    230         b = (298.082 * y + 516.411 * cb) 
    231  
    232         # clamp into range 
    233         r = min(max(r, 0), 65535) 
    234         g = min(max(g, 0), 65535) 
    235         b = min(max(b, 0), 65535) 
    236  
    237         # and return the color 
    238         return self.screen.get_color((r, g, b)) 
     250        return client.screen.get_color((r * 65535, g * 65535, b * 65535)) 
     251