Repository: qtile/qtile

Issue:
CurrentLayoutIcon failing wiith custom layout names
### The issue:

Qtile version: 0.21.0

Relevant logs:
```
2022-06-25 11:37:44,763 ERROR libqtile currentlayout.py:draw():L146 No icon for layout shiftytile
2022-06-25 11:37:47,899 ERROR libqtile currentlayout.py:draw():L146 No icon for layout monadtile
```

(EDIT: My browser ate the rest of this issue. Rewriting.)

I have a couple of tile layouts with custom names and parameters:
```python
    layout.Tile(name='monadtile',
                add_on_top=False,
                shift_windows=False,
                margin_on_single=False,
                border_on_single=False,
                expand=True,
                ratio=0.47,
                **layout_theme),
    layout.Columns(**layout_theme),
    layout.Tile(name='shiftytile',
                add_on_top=True,
                shift_windows=True,
                margin_on_single=False,
                expand=False,
                **layout_theme),
```

The correspondiing widget config is
```python
        widget.CurrentLayoutIcon(
            custom_icon_paths = [os.path.expanduser("~/.config/qtile/icons")],
            foreground = colors[0],
            background = pair.cols[0],
            padding = 0,
            scale = 0.6
        ),
```

I have icons named `layout-monadtile.png` and `layout-shiftytile.png` in `~/.config/qtile/icons`. They don't show up, and I get the above messages in qtile.log.

The problem is the following. The widget (`libqtile/widget/currentlayout.py`) obtains a list of layout names using 
```python
    def _get_layout_names(self):
        """
        Returns the list of lowercased strings for each available layout name.
        """
        return [layout.__class__.__name__.lower() for layout in self.qtile.config.layouts]
```

I logged it to see what it finds:
``` python
logger.warn(",".join([layout.__class__.__name__.lower() for layout in qtile.config.layouts]))
```

And it finds this:
``` python
2022-06-25 12:20:05,007 WARNING libqtile groups.py:layout_names_test():L51 tile,columns,tile
```

and not `monadtile,columns,shiftytile`. I changed `_get_layout_names` to
```python
    def _get_layout_names(self):
        """
        Returns the list of lowercased strings for each available layout name.
        """
        return [layout.name.lower() for layout in self.qtile.config.layouts]
        
```

and the icons for all layouts (including the custom `monadtile` and `shiftytile`) now show up. However I'm guessing there's a reason to use `layout.__class__.__name__` instead of `layout.name`. Any ideas?


### Required:

- [X] I have searched past issues to see if this bug has already been reported.



Discussion:
Have you created icons for your layouts? If not, the widget can't display them.

Separately, if you've got custom layouts, are these something that you think could be submitted as new features?
I had an extensive write-up of the problem and the solution but my browser ate most of it. :(

Please give me 5 minutes to rewrite.
I've updated the entry with details of the error.
Ah, ok. That makes more sense.

One approach could be to look up an icon for the named instance (in case the user has created one), if it's not found then we fall back to `layout.__class__.__name__.lower()`.

Would that work for you?
Yeah, that should work fine. But I don't understand why `layout.__class__.__name__` is needed. In the base layout class's `__init__` method, you have

``` python
        # name is a little odd; we can't resolve it until the class is defined
        # (i.e., we can't figure it out to define it in Layout.defaults), so
        # we resolve it here instead.
        if "name" not in config:
            config["name"] = self.__class__.__name__.lower() 
```

So `layout.name` should always work?
Not sure I follow. You've overriden `name` in your config so now `name` is `shiftytile` or whatever. So the widget looks for an image called `layout-shiftytile` and can't find it.

We set a default `name` for layouts as it's needed elsewhere (e.g. when limiting `lazy` calls with `.when(layout=name)`.
> So the widget looks for an image called `layout-shiftytile` and can't find it.

As I understand it this isn't what's happening. `layout-shiftytile.png` actually exists in my icons directory, so it never gets to the point of looking for it. Instead,

- `shiftytile` is never added to the list of names it expects to find (in `_setup_images`), and
- `shiftytile` is matched against by the `draw` method.

Moreover, changing `_get_layout_names` to

``` python
    def _get_layout_names(self):
        """
        Returns the list of lowercased strings for each available layout name.
        """
        return [layout.name.lower() for layout in self.qtile.config.layouts]
```
makes the icons appear correctly for _all_ the layouts, including the ones without custom names. I'm guessing this is because of the code in the `__init__` method for the base layout class I pasted above.
Ok, so it's a bug.

Are you happy to submit a PR to fix the initial lookup of layout names in `_setup_images` and do the fallback that we discussed above?
The fix would be in `_get_layout_names` (which `_setup_images` calls), as I understand it.

``` python
    def _get_layout_names(self):
        """
        Returns the list of lowercased strings for each available layout name.
        """
        names = []
        for layout in self.qtile.config.layouts:
            if layout.name is not None:
                names.append(layout.name.lower())
            else:
                names.append(layout.__class__.__name__.lower())
        return names
```

I don't know how to write non-trivial list comprehensions in Python, is the above okay?

Also, I still don't understand why the check is necessary, since every layout has a valid `name` attribute because of the code in the `__init__` method of the class `Layout`. But I guess this future-proofs it a little in case the `Layout` class is changed.
Every layout has a name but not every layout name has a matching icon. You need to cover the case where a layout has been renamed but there is only an icon file for the underlying class name.

The widget has some other issues that could be fixed too so I'll pick this up.

Resolving patch:
diff --git a/libqtile/widget/currentlayout.py b/libqtile/widget/currentlayout.py
--- a/libqtile/widget/currentlayout.py
+++ b/libqtile/widget/currentlayout.py
@@ -156,9 +156,12 @@ def draw(self):
 
     def _get_layout_names(self):
         """
-        Returns the list of lowercased strings for each available layout name.
+        Returns a list of tuples of lowercased layout name and class name strings for each available layout.
         """
-        return [layout.__class__.__name__.lower() for layout in self.qtile.config.layouts]
+        return [
+            (layout.name, layout.__class__.__name__.lower())
+            for layout in self.qtile.config.layouts
+        ]
 
     def _update_icon_paths(self):
         self.icon_paths = []
@@ -187,9 +190,18 @@ def _setup_images(self):
         """
         Loads layout icons.
         """
-        for layout_name in self._get_layout_names():
-            icon_file_path = self.find_icon_file_path(layout_name)
-            if icon_file_path is None:
+        for names in self._get_layout_names():
+            layout_name = names[0]
+            # Python doesn't have an ordered set but we can use a dictionary instead
+            # First key is the layout's name (which may have been set by the user),
+            # the second is the class name. If these are the same (i.e. the user hasn't
+            # set a name) then there is only one key in the dictionary.
+            layouts = dict.fromkeys(names)
+            for layout in layouts.keys():
+                icon_file_path = self.find_icon_file_path(layout)
+                if icon_file_path:
+                    break
+            else:
                 logger.warning('No icon found for layout "%s"', layout_name)
                 icon_file_path = self.find_icon_file_path("unknown")
 


Test patch: