├── _templates ├── widgets │ ├── panel-background.svg │ ├── plasmoidheading.svg │ └── background.svg ├── tabbar-solid.svg ├── tabbar-breeze.svg └── dialogs │ └── background.svg ├── widgets ├── pager.svgz ├── plasmoidheading.svg └── tabbar.svg ├── test ├── .gitignore ├── metadata.desktop ├── colors ├── ReadMe.md ├── Changelog.md ├── removeSvgTransforms.py ├── dialogs └── background.svg └── desktoptheme.py /_templates/widgets/panel-background.svg: -------------------------------------------------------------------------------- 1 | ../dialogs/background.svg -------------------------------------------------------------------------------- /widgets/pager.svgz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Zren/breeze-alphablack/HEAD/widgets/pager.svgz -------------------------------------------------------------------------------- /test: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | rm ~/.cache/plasma-svgelements-breeze-alphablack_* 4 | rm ~/.cache/plasma_theme_breeze-alphablack_*.kcache 5 | killall plasmashell; kstart5 plasmashell 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /widgets/background.svg 2 | /widgets/background.svgz 3 | /widgets/panel-background.svg 4 | /widgets/panel-background.svgz 5 | /breeze-alphablack-v*.zip 6 | __pycache__ 7 | /config.ini 8 | -------------------------------------------------------------------------------- /metadata.desktop: -------------------------------------------------------------------------------- 1 | [Desktop Entry] 2 | Comment=A semi-transparent black theme based on top of the default Breeze theme. 3 | Name=Breeze AlphaBlack 4 | X-KDE-PluginInfo-Author=KDE Visual Design Group + Zren 5 | X-KDE-PluginInfo-Category=Plasma Theme 6 | X-KDE-PluginInfo-Depends= 7 | X-KDE-PluginInfo-Email=zrenfire@gmail.com 8 | X-KDE-PluginInfo-EnabledByDefault=true 9 | X-KDE-PluginInfo-License=LGPL 10 | X-KDE-PluginInfo-Name=breeze-alphablack 11 | X-KDE-PluginInfo-Version=20 12 | X-KDE-PluginInfo-Website=https://github.com/Zren/breeze-alphablack 13 | X-Plasma-API=5.0 14 | 15 | X-KPackage-Dependencies=kns://plasmoids.knsrc/api.kde-look.org/1237963 16 | 17 | [ContrastEffect] 18 | enabled=true 19 | contrast=1.0 20 | intensity=1.0 21 | saturation=1.0 22 | 23 | [AdaptiveTransparency] 24 | enabled=true 25 | -------------------------------------------------------------------------------- /colors: -------------------------------------------------------------------------------- 1 | [ColorEffects:Disabled] 2 | Color=56,56,56 3 | ColorAmount=0 4 | ColorEffect=0 5 | ContrastAmount=0.65 6 | ContrastEffect=1 7 | IntensityAmount=0.1 8 | IntensityEffect=2 9 | 10 | [ColorEffects:Inactive] 11 | ChangeSelectionColor=true 12 | Color=112,111,110 13 | ColorAmount=0.025 14 | ColorEffect=2 15 | ContrastAmount=0.1 16 | ContrastEffect=2 17 | Enable=false 18 | IntensityAmount=0 19 | IntensityEffect=0 20 | 21 | [Colors:Button] 22 | BackgroundAlternate=23,23,23 23 | BackgroundNormal=17,17,17 24 | DecorationFocus=61,174,230 25 | DecorationHover=61,174,230 26 | ForegroundActive=246,116,0 27 | ForegroundInactive=175,176,179 28 | ForegroundLink=61,174,230 29 | ForegroundNegative=237,21,21 30 | ForegroundNeutral=201,206,59 31 | ForegroundNormal=239,240,241 32 | ForegroundPositive=17,209,22 33 | ForegroundVisited=61,174,230 34 | 35 | [Colors:Header] 36 | BackgroundAlternate=23,23,23 37 | BackgroundNormal=0,0,0 38 | DecorationFocus=61,174,230 39 | DecorationHover=61,174,230 40 | ForegroundActive=246,116,0 41 | ForegroundInactive=175,176,179 42 | ForegroundLink=61,174,230 43 | ForegroundNegative=237,21,21 44 | ForegroundNeutral=201,206,59 45 | ForegroundNormal=239,240,241 46 | ForegroundPositive=17,209,22 47 | ForegroundVisited=61,174,230 48 | 49 | [Colors:Selection] 50 | BackgroundAlternate=48,138,183 51 | BackgroundNormal=61,174,230 52 | DecorationFocus=61,174,230 53 | DecorationHover=61,174,230 54 | ForegroundActive=246,116,0 55 | ForegroundInactive=146,204,230 56 | ForegroundLink=252,252,252 57 | ForegroundNegative=237,21,21 58 | ForegroundNeutral=201,206,59 59 | ForegroundNormal=252,252,252 60 | ForegroundPositive=17,209,22 61 | ForegroundVisited=252,252,252 62 | 63 | [Colors:Tooltip] 64 | BackgroundAlternate=59,64,69 65 | BackgroundNormal=49,54,59 66 | DecorationFocus=61,174,230 67 | DecorationHover=61,174,230 68 | ForegroundActive=246,116,0 69 | ForegroundInactive=175,176,179 70 | ForegroundLink=61,174,230 71 | ForegroundNegative=237,21,21 72 | ForegroundNeutral=201,206,59 73 | ForegroundNormal=239,240,241 74 | ForegroundPositive=17,209,22 75 | ForegroundVisited=61,174,230 76 | 77 | [Colors:View] 78 | BackgroundAlternate=23,23,23 79 | BackgroundNormal=0,0,0 80 | DecorationFocus=61,174,230 81 | DecorationHover=61,174,230 82 | ForegroundActive=246,116,0 83 | ForegroundInactive=175,176,179 84 | ForegroundLink=61,174,230 85 | ForegroundNegative=237,21,21 86 | ForegroundNeutral=201,206,59 87 | ForegroundNormal=239,240,241 88 | ForegroundPositive=17,209,22 89 | ForegroundVisited=61,174,230 90 | 91 | [Colors:Window] 92 | BackgroundAlternate=23,23,23 93 | BackgroundNormal=0,0,0 94 | DecorationFocus=61,174,230 95 | DecorationHover=61,174,230 96 | ForegroundActive=246,116,0 97 | ForegroundInactive=175,176,179 98 | ForegroundLink=61,174,230 99 | ForegroundNegative=237,21,21 100 | ForegroundNeutral=201,206,59 101 | ForegroundNormal=239,240,241 102 | ForegroundPositive=17,209,22 103 | ForegroundVisited=61,174,230 104 | 105 | [Colors:Complementary] 106 | BackgroundAlternate=23,23,23 107 | BackgroundNormal=17,17,17 108 | DecorationFocus=30,146,255 109 | DecorationHover=61,174,230 110 | ForegroundActive=246,116,0 111 | ForegroundInactive=175,176,179 112 | ForegroundLink=61,174,230 113 | ForegroundNegative=237,21,21 114 | ForegroundNeutral=201,206,59 115 | ForegroundNormal=239,240,241 116 | ForegroundPositive=17,209,22 117 | ForegroundVisited=61,174,230 118 | 119 | [General] 120 | ColorScheme=Breeze AlphaBlack 121 | Name=Breeze AlphaBlack 122 | shadeSortColumn=true 123 | 124 | [KDE] 125 | contrast=4 126 | 127 | -------------------------------------------------------------------------------- /ReadMe.md: -------------------------------------------------------------------------------- 1 | # Breeze AlphaBlack 2 | 3 | Breeze Light theme with minor improvements and a black panel/titlebar. 4 | 5 | https://store.kde.org/p/1084931/ 6 | 7 | ## Screenshots 8 | 9 | ![](https://cdn.pling.com/img/f/1/e/1/7f50888757a024f34ef3906c0948817605b3.png) 10 | 11 | ## Install 12 | 13 | * System Settings > Desktop Theme 14 | * Get New Themes... 15 | * Search for "Breeze AlphaBlack", install it, then apply it. 16 | * If you want the breeze window decorations in black, run `python3 ~/.local/share/plasma/desktoptheme/breeze-alphablack/setthemecolor.py 0,0,0` 17 | 18 | ## Customize Theme 19 | 20 | ### "AlphaBlack Control" Widget 21 | 22 | ![](https://i.imgur.com/TYxCBnc.jpg) 23 | 24 | v10 introduced an accompanying widget to easily change the theme's accent color, panel opacity, or change the taskbar to look a bit more like Windows 10. 25 | 26 | You can [download the widget here](https://store.kde.org/p/1237963/), or install it via the GUI: 27 | 28 | 1. Right click the Panel > Add Widgets 29 | 2. Get New Widgets > Download 30 | 3. Search for "AlphaBlack Control" and install it. 31 | 32 | After installing, the widget should appear in your system tray. If you lock your widgets, it will hide in the system tray popup. 33 | 34 | 35 | ### Command Line 36 | 37 | Run `python3 ~/.local/share/plasma/desktoptheme/breeze-alphablack/desktoptheme.py` 38 | to see all commands in the help message. 39 | 40 | #### Panel Opacity 41 | 42 | Run `python3 ~/.local/share/plasma/desktoptheme/breeze-alphablack/desktoptheme.py set panel.opacity 0.75` 43 | where `0.75` means the panel is 75% visible. 44 | 45 | You can also set the `dialog.opacity` for panel popups, and `widget.opacity` for desktop widgets. 46 | 47 | 48 | #### Panel Color + Window Decorations 49 | 50 | Run `python3 ~/.local/share/plasma/desktoptheme/breeze-alphablack/desktoptheme.py set theme.accentColor 255,255,255` 51 | where `255,255,255` is your desired RGB color. 52 | 53 | 54 | #### Window Decorations 55 | 56 | Run `python3 ~/.local/share/plasma/desktoptheme/breeze-alphablack/desktoptheme.py settitlebarcolor 255,255,255` 57 | where `255,255,255` is your desired RGB color. 58 | 59 | Run `python3 ~/.local/share/plasma/desktoptheme/breeze-alphablack/desktoptheme.py resettitlebarcolors` 60 | to reapply the colors from your seleted color scheme. 61 | 62 | #### Task Manager Theme 63 | 64 | Run `python3 ~/.local/share/plasma/desktoptheme/breeze-alphablack/settasksvg.py outside` 65 | where `outside` draws the line on the edge of the screen like Windows 10, or `inside` like Breeze. 66 | 67 | 68 | 69 | ## Window Decorations 70 | 71 | To color the the default breeze window decorations (window titlebars). You can either: 72 | 73 | 1. Download a custom window decoration. 74 | 2. Use the set titlebar colors python script above. 75 | 3. Use the System Settings > Colors > Colors section to color the titlebar background/foreground. You will not be able to color the frame color (bottom/left/right of the window) from this menu. 76 | 4. Add/set the following values in your `~/.config/kdeglobals` file to set the borders to black (when using the breeze window decorations). 77 | 78 | ```ini 79 | [WM] 80 | activeBackground=0,0,0 81 | activeBlend=17,17,17 82 | activeFont=Noto Sans,10,-1,5,50,0,0,0,0,0 83 | activeForeground=239,240,241 84 | inactiveBackground=8,8,8 85 | inactiveBlend=75,71,67 86 | inactiveForeground=189,195,199 87 | frame=0,0,0 88 | inactiveFrame=8,8,8 89 | ``` 90 | 91 | ## Misc 92 | 93 | * Wiki for Editing Desktop Themes 94 | https://techbase.kde.org/Development/Tutorials/Plasma5/ThemeDetails 95 | * Code that parses the `colors` file for use with the svg CSS colors. 96 | https://github.com/KDE/plasma-framework/blob/master/src/plasma/private/theme_p.cpp#L422 97 | -------------------------------------------------------------------------------- /widgets/plasmoidheading.svg: -------------------------------------------------------------------------------- 1 | 2 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 56 | 64 | 72 | 80 | 81 | -------------------------------------------------------------------------------- /_templates/widgets/plasmoidheading.svg: -------------------------------------------------------------------------------- 1 | 2 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 56 | 64 | 72 | 80 | 81 | -------------------------------------------------------------------------------- /Changelog.md: -------------------------------------------------------------------------------- 1 | ## v20 - October 9 2021 2 | 3 | * Parse `[Sub][Sections]` in kdeglobals to fix `desktoptheme.py` commands. 4 | * Use dbus plasma script to quickly change theme faster. 5 | 6 | ## v19 - February 25 2021 7 | 8 | * Refactor `panel-background.svg` to reuse the `dialog/background.svg`. 9 | * Add thick margins to the panel svg to support Plasma 5.21's Margin Separator widgets. A 24px panel will have a 1px thick margin. A 30px panel will have a 4px thick margin. A 38px panel will have a 8px thick margin. 10 | 11 | ## v18 - December 11 2020 12 | 13 | * Fix pale popups caused by the Background Contrast Desktop Effect (Issue #21). Added `ContrastEffect` properties in `metadata.desktop` in an attempt to fix it, however the proper fix was to ship a pre-generated `dialogs/background.svg`. 14 | * Refactor `background.svg` to add `dialog.radius` which doesn't really work well. 15 | * Fix `resettitlebarcolors` mentioned in AlphaBlackControl's Issue 2. 16 | * Only use blue shifted DecorationFocus for Complementary colors (Issue #20) 17 | * Set Header color group to fix white `plasmoidheading.svg` in Plasma 5.20 18 | 19 | ## v17 - June 16 2020 20 | 21 | * Added `widgets/plasmoidheading.svg` to show the solid notification heading like Breeze. It's only displayed if the current theme has the svg, it does not inherit this svg. 22 | * Removed all deprecated `set___color.py` scripts. Use the `desktoptheme.py` subcommands. 23 | 24 | ## v16 - March 15 2020 25 | 26 | * Move `set___color.py` etc into `desktoptheme.py` as subcommands. Eg: `python3 desktoptheme.py set dialog.opacity 0.7`. The existing scripts are deprecated and will be removed in the next version. 27 | * Add ability to set dialog padding with `python3 desktoptheme.py set dialog.padding 2`. Note that you need to close a popup and reopen it to see the change in padding. You may need to run the command a few times for it to take effect. 28 | 29 | ## v15 - March 17 2019 30 | 31 | * Cleanup the desktop widget background svg coordinates (`widget/background.svg`). 32 | * Use Breeze's rectangle new rounded corner popup dialog svg from KDE Frameworks 5.56. 33 | * Fix the dialog popup color (when using a custom opacity) broken by the Qt 5.12.2 update. The default dialog popups and tooltips will be fixed in KDE Frameworks 5.56.2 or 5.57.0. 34 | 35 | ## v14 - December 15 2018 36 | 37 | * Use Breeze's rectangle corner popup svg. Somewhere along the line this theme ended up using a different svg with round corners for some reason. 38 | * Add `sethighlightcolor.py` to set the selection/highlight color. 39 | 40 | ## v13 - October 12 2018 41 | 42 | * Hide the panel shadow when the panel opacity is set below 30% (`0.3`). 43 | * Update the desktop widget background to use the same look as breeze. 44 | * Add `settextcolor.py` to change the panel/widget/titlebar text color. 45 | * Fix `resettitlebarcolor.py` not working for color schemes installed in the home directory (`~/.local/share/color-scheme/`. It previously only worked for color schemes in the root directory (`/usr/share/color-scheme/`). This fix also scans all color schemes when the color scheme filename is not the typical `{ColorSchemeName}.colors` (Eg: "Breeze Solarized Light" uses "BreezeSolarizedLight.colors"). 46 | * Add `resettodefaults.py` script to revert all changes. 47 | 48 | ## v12 - July 26 2018 49 | 50 | * Add settitlebarcolor.py and resettitlebarcolor.py to easily reset the titlebar color. v3 of the widget has buttons that will run these scripts. 51 | 52 | ## v11 - June 24 2018 53 | 54 | * Remove the opache and transparent folders which don't appear to do anything (they aren't overriding breeze). 55 | * Add new templates + scripts to set the panel popups/dialog opacity, and the desktop widget opacity. The new scripts are setdialogopacity.py and setwidgetopacity.py. Both work the same as setpanelopacity.py. 56 | 57 | ## v10 - May 29 2018 58 | 59 | * Follow color scheme when compositor is off. Gets rid of white outline when compositor is off. 60 | * Add breeze's widgets/tabbar.svg, as Plasma doesn't allow it to "inherit" this svg. This fixes/adds the "popup is open" blue line. 61 | * Add alternative tasks.svg where the line is on the outside/edge of the screen line Windows 10. Use the config widget, or the settasksvg.py script, to change to it. 62 | * "Unzip" the svgz into svg, and cleanup the xml. 63 | 64 | ## v9 - February 20 2017 65 | 66 | * Fix panel opacity script so it can be run anywhere. 67 | 68 | ## v8 - January 21 2017 69 | 70 | * Add a script to set the panel opacity. 71 | 72 | ## v7 73 | 74 | * Fix the minimized tasks svg (showed nothing instead of looking like a "normal" task) 75 | * Use a hardcoded #f67400 orange for highlighted tasks instead of the NeutralText color from the color scheme. 76 | 77 | ## v6 78 | 79 | * Use the Breeze 5.8 tasks.svg (thinner line), but with minimized tasks looking the same as a normal task. 80 | 81 | ## v5 82 | 83 | * Add script to change the color. 84 | 85 | 86 | ## v3 - June 16 2016 87 | 88 | * Theme progressbar in tasks. 89 | 90 | ## v2 - June 15 2016 91 | 92 | * Got rid of the white line at the top of the panel 93 | * Got rid of the pink spot in the pager 94 | -------------------------------------------------------------------------------- /_templates/tabbar-solid.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 18 | 37 | 48 | 49 | 51 | 85 | 95 | 96 | 98 | 99 | 101 | image/svg+xml 102 | 104 | 105 | 106 | 107 | 108 | 112 | 113 | 114 | 121 | 122 | 123 | 129 | 135 | 141 | 147 | 148 | 149 | 157 | 165 | 173 | 181 | 189 | 197 | 205 | 213 | 221 | 222 | 230 | 238 | 246 | 254 | 255 | 256 | 257 | 265 | 273 | 281 | 289 | 297 | 305 | 313 | 321 | 329 | 330 | 338 | 346 | 354 | 362 | 363 | 364 | 365 | 373 | 381 | 389 | 397 | 405 | 413 | 421 | 429 | 437 | 438 | 446 | 454 | 462 | 470 | 471 | 472 | 473 | 481 | 489 | 497 | 505 | 513 | 521 | 529 | 537 | 545 | 546 | 554 | 562 | 570 | 578 | 579 | 580 | 581 | 582 | -------------------------------------------------------------------------------- /removeSvgTransforms.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | 3 | import sys, os 4 | from bs4 import BeautifulSoup, NavigableString, Tag 5 | import re 6 | import math 7 | 8 | # s = '4.500' 9 | # while len(s) > 2: 10 | # if s[-1] == '0': 11 | # print(s, s[0:-1]) 12 | # s = s[0:-1] 13 | # sys.exit(0) 14 | 15 | def bringToFront(arr, value): 16 | try: 17 | i = arr.index(value) 18 | arr.insert(0, arr.pop(i)) 19 | except ValueError: 20 | pass # Not in the list 21 | 22 | def bringAllToFront(arr, keys): 23 | for key in reversed(keys): 24 | bringToFront(arr, key) 25 | 26 | def sortedAttrs(el): 27 | keys = el.attrs.keys() 28 | keys = sorted(keys) 29 | bringAllToFront(keys, [ 30 | "id", 31 | "class", 32 | "x", 33 | "y", 34 | "width", 35 | "height", 36 | "style", 37 | ]) 38 | for key in keys: 39 | yield key, el.attrs[key] 40 | 41 | 42 | def dent(indent): 43 | return " " * indent 44 | 45 | def nl(indent): 46 | return "\n" + dent(indent) 47 | 48 | def renderTag(el, indent=0): 49 | # s += (" " * indent) 50 | # print(dent(indent), el.name, type(el)) 51 | if type(el) == Tag or type(el) == BeautifulSoup: 52 | if el.name == 'metadata': 53 | return "" # Skip it (it has annoying namespaces and we don't need it anyways) 54 | 55 | s = "" 56 | s += "<" + el.name 57 | for key, value in sortedAttrs(el): 58 | s += nl(indent+1) + " " + str(key) + "=\"" + str(value) + "\"" 59 | 60 | childrenStr = "" 61 | childCount = 0 62 | for child in el.children: 63 | childStr = renderTag(child, indent+1) 64 | if len(childStr) > 0: 65 | childrenStr += nl(indent+1) + childStr 66 | childCount += 1 67 | 68 | if childCount == 0: 69 | s += " /" 70 | s += ">" 71 | 72 | s += childrenStr 73 | if childCount > 0: 74 | s += nl(indent) + "" 75 | return s 76 | elif type(el) == NavigableString: 77 | s = str(el) 78 | if s.strip() == '': 79 | return "" 80 | else: 81 | # print("LOG", s) 82 | return s 83 | else: 84 | print("ERROR", type(el), el) 85 | return "" 86 | 87 | def renderSvg(svg): 88 | s = '\n' 89 | s += renderTag(svg) 90 | s += '\n' 91 | return s 92 | 93 | def parseArgs(s): 94 | left = s.index('(') + 1 95 | right = s.index(')') 96 | s = s[left:right] 97 | # print(s) 98 | tokens = s.split(',') 99 | args = map(float, tokens) 100 | args = list(args) 101 | # print(args) 102 | return args 103 | 104 | def transformChildren(parent, func, *args): 105 | for el in parent.children: 106 | if type(el) == Tag: 107 | # print('\t', el) 108 | func(el, *args) 109 | 110 | def roundIfNeeded(a): 111 | # return a 112 | b = round(a) 113 | if a < b and b <= a + 0.01: 114 | return b # a ~= 0.99 115 | elif a - 0.01 <= b and b <= a: 116 | return b # a ~= 1.01 117 | else: 118 | return a 119 | 120 | def applyTranslate(el, dx, dy): 121 | # First check if the el has a tranlsate() that negates (dx,dy). 122 | transform = el.attrs.get('transform') 123 | if transform is not None and 'translate(' in transform: 124 | args = parseArgs(transform) 125 | dx2, dy2 = args 126 | if math.isclose(dx, -dx2) and math.isclose(dy, -dy2): 127 | del el.attrs['transform'] 128 | return 129 | 130 | # If not, shift all coordinates of the el. 131 | if el.name == 'g': 132 | transformChildren(el, applyTranslate, dx, dy) 133 | elif el.name == 'path': 134 | applyMatrixToPath(el, 1, 0, 0, 1, dx, dy) 135 | elif el.name == 'rect': 136 | applyTranslateToRect(el, dx, dy) 137 | else: 138 | raise Exception("Cannot applyTranslate to <{}>".format(el.name)) 139 | 140 | def formatNumber(x): 141 | if type(x) == float: 142 | s = str(x) 143 | a,b = s.split('.') 144 | if len(b) > 3: # significance >3 145 | s = "{:.3f}".format(x) 146 | while s[-1] == '0': 147 | # print(s, s[0:-1]) 148 | s = s[0:-1] 149 | return s 150 | else: 151 | return s 152 | else: 153 | return str(x) 154 | 155 | def applyTranslateToRect(el, dx, dy): 156 | x = float(el["x"]) 157 | y = float(el["y"]) 158 | 159 | x += dx 160 | y += dy 161 | # print("\t\t({}, {}) => ({}, {})".format(el["x"], el["y"], x, y)) 162 | 163 | x = formatNumber(roundIfNeeded(x)) 164 | y = formatNumber(roundIfNeeded(y)) 165 | # print("\t\t({}, {}) => ({}, {})".format(el["x"], el["y"], x, y)) 166 | 167 | el["x"] = x 168 | el["y"] = y 169 | 170 | 171 | def applyMatrix(el, a, b, c, d, e, f): 172 | if el.name == 'rect': 173 | applyMatrixToRect(el, a, b, c, d, e, f) 174 | elif el.name == 'path': 175 | applyMatrixToPath(el, a, b, c, d, e, f) 176 | else: 177 | raise Exception("Cannot applyMatrix to <{}>".format(el.name)) 178 | 179 | def applyMatrixToRect(el, a, b, c, d, e, f): 180 | # print("\t{}".format(el.parent['id'])) 181 | x1 = float(el["x"]) 182 | y1 = float(el["y"]) 183 | w1 = float(el["width"]) 184 | h1 = float(el["height"]) 185 | x2 = x1 + w1 186 | y2 = y1 + h1 187 | # print("\t\t({}, {}, {}, {})".format(el["x"], el["y"], el["width"], el["height"])) 188 | 189 | # https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/transform 190 | x3, y3 = applyMatrixToPoint(x1, y1, a, b, c, d, e, f) 191 | x4, y4 = applyMatrixToPoint(x2, y2, a, b, c, d, e, f) 192 | w2 = x4 - x3 193 | h2 = y4 - y3 194 | # print("\t\t\t=> ({}, {}, {}, {})".format(x3, y3, w2, h2)) 195 | 196 | if w2 < 0: 197 | x3 = x4 198 | # x3 += w2 199 | w2 *= -1 200 | if h2 < 0: 201 | y3 = y4 202 | # y3 += h2 203 | h2 *= -1 204 | 205 | x3 = roundIfNeeded(x3) 206 | y3 = roundIfNeeded(y3) 207 | w2 = roundIfNeeded(w2) 208 | h2 = roundIfNeeded(h2) 209 | # print("\t\t\t=> ({}, {}, {}, {})".format(x3, y3, w2, h2)) 210 | 211 | el["x"] = formatNumber(x3) 212 | el["y"] = formatNumber(y3) 213 | el["width"] = formatNumber(w2) 214 | el["height"] = formatNumber(h2) 215 | 216 | def applyScaleToRect(el, sx, sy): 217 | applyMatrixToRect(el, sx, 0, 0, sy, 0, 0) 218 | 219 | 220 | # def applyMatrixToPoint(x1, y1, a, b, c, d, e, f): 221 | # x1 = float(x1) 222 | # y1 = float(y1) 223 | 224 | # # https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/transform 225 | # x2 = a*x1 + c*y1 + e 226 | # y2 = b*x1 + d*y1 + f 227 | 228 | # x2 = roundIfNeeded(x2) 229 | # y2 = roundIfNeeded(y2) 230 | 231 | # x2 = formatNumber(x2) 232 | # y2 = formatNumber(y2) 233 | # # print("\t\t({}, {}) => ({}, {})".format(x1, y1, x2, y2)) 234 | 235 | # return x2, y2 236 | 237 | def applyMatrixToPoint(x1, y1, a, b, c, d, e, f): 238 | # https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/transform 239 | x2 = a*x1 + c*y1 + e 240 | y2 = b*x1 + d*y1 + f 241 | return x2, y2 242 | 243 | def applyMatrixToPath(el, a, b, c, d, e, f): 244 | # print("\t{}".format(el.parent['id'])) 245 | # print("matrix({}, {}, {}, {}, {}, {})".format(a, b, c, d, e, f)) 246 | path = el['d'] 247 | path = path.replace(',', ' ') 248 | tokens = path.split(' ') 249 | out = [] 250 | lastCommand = '' 251 | command = '' 252 | 253 | matrixIsTranslation = a == 1 and b == 0 and c == 0 and d == 1 # With (e == dx, f == dy) 254 | 255 | tokenIter = iter(tokens) 256 | commandUseCount = 0 257 | arcX1 = 0 258 | arcY1 = 0 259 | arcX2 = 0 260 | arcY2 = 0 261 | 262 | # https://developer.mozilla.org/en-US/docs/Web/SVG/Tutorial/Paths 263 | pathCommands = [ 264 | 'M', 'L', 'H', 'V', 'Z', 'C', 'S', 'Q', 'T', 'A', # M x,y (absolute coordinates) 265 | 'm', 'l', 'h', 'v', 'z', 'c', 's', 'q', 't', 'a', # m dx,dy (relative) 266 | ] 267 | for token in tokenIter: 268 | # print('\t\t' + token) 269 | 270 | if token in pathCommands: 271 | commandUseCount = 0 272 | lastCommand = command 273 | command = token 274 | if command.lower() == 'z': # Close Path 275 | out.append('z') 276 | continue 277 | 278 | commandUseCount += 1 279 | 280 | if command == 'm' or command == 'l': 281 | # Move To x,y 282 | # Line to x,y 283 | x = token 284 | y = next(tokenIter) 285 | x1 = float(x) 286 | y1 = float(y) 287 | 288 | if commandUseCount == 1: 289 | # Remember original x,y positions 290 | arcX1 = x1 291 | arcY1 = y1 292 | elif commandUseCount >= 2: 293 | # x,y is delta dx,dy 294 | x1 = arcX1 + x1 295 | y1 = arcY1 + y1 296 | arcX1 = x1 297 | arcY1 = y1 298 | 299 | x2, y2 = applyMatrixToPoint(x1, y1, a, b, c, d, e, f) 300 | 301 | if commandUseCount == 1: 302 | # Remember matrixed x,y positions 303 | arcX2 = x2 304 | arcY2 = y2 305 | x2 = formatNumber(roundIfNeeded(x2)) 306 | y2 = formatNumber(roundIfNeeded(y2)) 307 | commandStr = "{} {},{}".format(command, x2, y2) 308 | elif commandUseCount >= 2: 309 | # Generate matrixed delta dx,dy 310 | dx = x2 - arcX2 311 | dy = y2 - arcY2 312 | dx = formatNumber(roundIfNeeded(dx)) 313 | dy = formatNumber(roundIfNeeded(dy)) 314 | arcX2 = x2 315 | arcY2 = y2 316 | commandStr = "{},{}".format(dx, dy) 317 | # print("\t\t[{}] ({},{}) => ({})".format(command, x, y, commandStr)) 318 | out.append(commandStr) 319 | 320 | 321 | #--- Abosolute coordinates 322 | elif command == 'H': # Horizontal to x 323 | # H x 324 | raise Exception("Implement path Bezier Curves") 325 | elif command == 'V': # Vertical to y 326 | # V y 327 | raise Exception("Implement path Bezier Curves") 328 | elif command == 'Z': # Close Path 329 | raise Exception('Close Path does not use arguments') 330 | elif command == 'C': # Cubic Bezier curve to 331 | # C x1 y1, x2 y2, x y 332 | raise Exception("Implement '{}' {}. el: {}".format(command, 'path Bezier Curves', el)) 333 | elif command == 'S': # Continue Cubic Bezier curve 334 | # S x2 y2, x y 335 | raise Exception("Implement '{}' {}. el: {}".format(command, 'path Bezier Curves', el)) 336 | elif command == 'Q': # Quadratic Bezier curve 337 | # Q x1 y1, x y 338 | raise Exception("Implement '{}' {}. el: {}".format(command, 'path Bezier Curves', el)) 339 | elif command == 'T': # Continue Quadratic Bezier curve 340 | # T x y 341 | raise Exception("Implement '{}' {}. el: {}".format(command, 'path Bezier Curves', el)) 342 | elif command == 'A': # Arc to 343 | # A rx ry x-axis-rotation large-arc-flag sweep-flag x y 344 | raise Exception("Implement '{}' {}. el: {}".format(command, 'path Arc', el)) 345 | 346 | 347 | #--- Relative coordinates 348 | elif command == 'h': # Horizontal to x 349 | # h dx 350 | dx = float(token) 351 | if matrixIsTranslation: 352 | pass # Only uses delta coordinates (which we can ignore) 353 | else: 354 | raise Exception("Implement '{}' {}. el: {}".format(command, 'path h/v', el)) 355 | elif command == 'v': # Vertical to y 356 | # v dy 357 | dy = float(token) 358 | if matrixIsTranslation: 359 | pass # Only uses delta coordinates (which we can ignore) 360 | else: 361 | raise Exception("Implement '{}' {}. el: {}".format(command, 'path h/v', el)) 362 | elif command == 'z': # Close Path 363 | raise Exception('Close Path does not use arguments {}'.format(el)) 364 | elif command == 'c': # Cubic Bezier curve to 365 | # c dx1 dy1, dx2 dy2, dx dy 366 | dx1 = float(token) 367 | dy1 = float(next(tokenIter)) 368 | dx2 = float(next(tokenIter)) 369 | dy2 = float(next(tokenIter)) 370 | dx = float(next(tokenIter)) 371 | dy = float(next(tokenIter)) 372 | if matrixIsTranslation: 373 | pass # Only uses delta coordinates (which we can ignore) 374 | else: 375 | raise Exception("Implement '{}' {}. el: {}".format(command, 'path Bezier Curves', el)) 376 | elif command == 's': # Continue Cubic Bezier curve 377 | # s dx2 dy2, dx dy 378 | dx2 = float(token) 379 | dy2 = float(next(tokenIter)) 380 | dx = float(next(tokenIter)) 381 | dy = float(next(tokenIter)) 382 | if matrixIsTranslation: 383 | pass # Only uses delta coordinates (which we can ignore) 384 | else: 385 | raise Exception("Implement '{}' {}. el: {}".format(command, 'path Bezier Curves', el)) 386 | elif command == 'q': # Quadratic Bezier curve 387 | # q dx1 dy1, dx dy 388 | dx1 = float(token) 389 | dy1 = float(next(tokenIter)) 390 | dx = float(next(tokenIter)) 391 | dy = float(next(tokenIter)) 392 | if matrixIsTranslation: 393 | pass # Only uses delta coordinates (which we can ignore) 394 | else: 395 | raise Exception("Implement '{}' {}. el: {}".format(command, 'path Bezier Curves', el)) 396 | elif command == 't': # Continue Quadratic Bezier curve 397 | # t dx dy 398 | dx = float(token) 399 | dy = float(next(tokenIter)) 400 | if matrixIsTranslation: 401 | pass # Only uses delta coordinates (which we can ignore) 402 | else: 403 | raise Exception("Implement '{}' {}. el: {}".format(command, 'path Bezier Curves', el)) 404 | elif command == 'a': # Arc to 405 | # A rx ry x-axis-rotation large-arc-flag sweep-flag x y 406 | # a 4.5,4.5 0 0 0 -4.5,4.5 407 | rx = token 408 | ry = next(tokenIter) 409 | xAxisRotation = next(tokenIter) 410 | largeArcFlag = next(tokenIter) 411 | sweepFlag = next(tokenIter) 412 | x = next(tokenIter) 413 | y = next(tokenIter) 414 | 415 | rx1 = float(rx) 416 | ry1 = float(ry) 417 | x1 = float(x) 418 | y1 = float(y) 419 | 420 | 421 | if commandUseCount >= 2 or lastCommand == 'm': 422 | # x,y is delta dx,dy 423 | x1 = arcX1 + x1 424 | y1 = arcY1 + y1 425 | arcX1 = x1 426 | arcY1 = y1 427 | else: # commandUseCount == 1 428 | # Remember original x,y positions 429 | arcX1 = x1 430 | arcY1 = y1 431 | # pass 432 | 433 | # rx2, ry2 = applyMatrixToPoint(rx1, ry1, a, b, c, d, e, f) 434 | rx2 = a*rx1 + c*ry1 435 | ry2 = b*rx1 + d*ry1 436 | # rx2 = a*rx1 437 | # ry2 = d*ry1 438 | x2, y2 = applyMatrixToPoint(x1, y1, a, b, c, d, e, f) 439 | 440 | if rx2 < 0: 441 | rx2 *= -1 442 | x2 -= rx2 * 2 443 | if ry2 < 0: 444 | ry2 *= -1 445 | y2 -= ry2 * 2 446 | 447 | if commandUseCount >= 2 or lastCommand == 'm': 448 | # Generate matrixed delta dx,dy 449 | dx = x2 - arcX2 450 | dy = y2 - arcY2 451 | dx = formatNumber(roundIfNeeded(dx)) 452 | dy = formatNumber(roundIfNeeded(dy)) 453 | rx2 = formatNumber(roundIfNeeded(rx2)) 454 | ry2 = formatNumber(roundIfNeeded(ry2)) 455 | arcX2 = x2 456 | arcY2 = y2 457 | commandStr = "{},{} {} {} {} {},{}".format(rx2, ry2, xAxisRotation, largeArcFlag, sweepFlag, dx, dy) 458 | else: # commandUseCount == 1 459 | # Remember matrixed x,y positions 460 | arcX2 = x2 461 | arcY2 = y2 462 | x2 = formatNumber(roundIfNeeded(x2)) 463 | y2 = formatNumber(roundIfNeeded(y2)) 464 | rx2 = formatNumber(roundIfNeeded(rx2)) 465 | ry2 = formatNumber(roundIfNeeded(ry2)) 466 | commandStr = "{},{} {} {} {} {},{}".format(rx2, ry2, xAxisRotation, largeArcFlag, sweepFlag, x2, y2) 467 | 468 | if lastCommand != 'a': 469 | commandStr = 'a ' + commandStr 470 | 471 | # print("\t\t({},{} ... {},{}) => ({})".format(rx, ry, x, y, commandStr)) 472 | # print("\t\t[a] ({},{}) => ({})".format(x, y, commandStr)) 473 | out.append(commandStr) 474 | 475 | # raise Exception("Implement path Arc to") 476 | 477 | 478 | el['d'] = ' '.join(out) 479 | 480 | 481 | def removeGroupTransforms(svg): 482 | for g in svg.find_all('g'): 483 | transform = g.attrs.get('transform') 484 | # print(transform) 485 | # TODO: Support multiple functions in 1 tag. 486 | if transform is None or transform.strip() == '': 487 | continue 488 | elif 'translate(' in transform: 489 | # translateChildren() 490 | args = parseArgs(transform) 491 | transformChildren(g, applyTranslate, *args) 492 | del g.attrs['transform'] 493 | elif 'matrix(' in transform: 494 | # translateChildren() 495 | args = parseArgs(transform) 496 | transformChildren(g, applyMatrix, *args) 497 | del g.attrs['transform'] 498 | else: 499 | raise Exception('Unsupported transform', transform) 500 | 501 | def removeRectTransforms(svg): 502 | for rect in svg.find_all('rect'): 503 | # print(rect) 504 | transform = rect.attrs.get('transform') 505 | # print(transform) 506 | # TODO: Support multiple functions in 1 tag. 507 | if transform is None or transform.strip() == '': 508 | continue 509 | elif 'scale(' in transform: 510 | args = parseArgs(transform) 511 | applyScaleToRect(rect, *args) 512 | del rect.attrs['transform'] 513 | elif 'matrix(' in transform: 514 | args = parseArgs(transform) 515 | applyMatrixToRect(rect, *args) 516 | del rect.attrs['transform'] 517 | else: 518 | raise Exception('Unsupported transform', transform) 519 | 520 | def removeTransforms(svg): 521 | removeRectTransforms(svg) 522 | removeGroupTransforms(svg) 523 | 524 | 525 | def printHelp(): 526 | print('python3 removeSvgTransforms.py [svgFile]') 527 | print('\tEg: python3 removeSvgTransforms.py _templates/tasks-outside.svg') 528 | 529 | if __name__ == '__main__': 530 | if len(sys.argv) >= 2: 531 | inSvgPath = sys.argv[1] 532 | outSvgPath = sys.argv[1] 533 | else: 534 | if False: 535 | inSvgPath = '_templates/tasks-outside.svg' 536 | outSvgPath = 'test.svg' 537 | else: 538 | printHelp() 539 | sys.exit(1) 540 | 541 | with open(inSvgPath, 'r') as fin: 542 | soup = BeautifulSoup(fin, 'xml') 543 | svg = soup.find('svg') 544 | removeTransforms(svg) 545 | s = renderSvg(svg) 546 | # print(s) 547 | 548 | with open(outSvgPath, 'w') as fout: 549 | fout.write(s) 550 | -------------------------------------------------------------------------------- /_templates/widgets/background.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | image/svg+xml 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | -------------------------------------------------------------------------------- /dialogs/background.svg: -------------------------------------------------------------------------------- 1 | 13 | 14 | 60 | 61 | 66 | 71 | 72 | 73 | 78 | 83 | 84 | 94 | 105 | 115 | 126 | 136 | 146 | 157 | 168 | 169 | 196 | 206 | 207 | 208 | 209 | 210 | image/svg+xml 211 | 212 | 213 | 214 | 215 | 216 | 217 | 218 | 226 | 234 | 235 | 236 | 237 | 238 | 239 | 240 | 246 | 247 | 248 | 257 | 258 | 259 | 265 | 266 | 267 | 276 | 277 | 278 | 287 | 288 | 289 | 298 | 299 | 300 | 306 | 307 | 308 | 317 | 318 | 319 | 325 | 326 | 327 | 328 | 336 | 344 | 352 | 360 | 361 | 362 | 370 | 378 | 386 | 394 | 395 | 396 | 397 | 398 | 399 | 407 | 412 | 420 | 421 | 422 | 430 | 438 | 439 | 440 | 448 | 453 | 461 | 462 | 463 | 471 | 479 | 480 | 481 | 489 | 490 | 491 | 499 | 507 | 508 | 509 | 517 | 522 | 530 | 531 | 532 | 540 | 548 | 549 | 550 | 558 | 563 | 571 | 572 | 573 | 574 | 582 | 590 | 598 | 606 | 607 | 608 | 616 | 624 | 632 | 640 | 641 | 642 | 643 | 644 | 645 | 650 | 651 | 652 | 660 | 661 | 662 | 667 | 668 | 669 | 677 | 678 | 679 | 687 | 688 | 689 | 697 | 698 | 699 | 704 | 705 | 706 | 714 | 715 | 716 | 721 | 722 | 723 | 724 | 725 | -------------------------------------------------------------------------------- /widgets/tabbar.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 18 | 37 | 48 | 49 | 51 | 85 | 86 | 88 | 89 | 91 | image/svg+xml 92 | 94 | 95 | 96 | 97 | 98 | 102 | 109 | 113 | 121 | 122 | 126 | 133 | 141 | 142 | 146 | 153 | 161 | 162 | 170 | 178 | 186 | 194 | 202 | 210 | 217 | 224 | 231 | 238 | 247 | 256 | 265 | 274 | 283 | 292 | 300 | 308 | 312 | 320 | 321 | 329 | 337 | 346 | 355 | 364 | 373 | 382 | 391 | 399 | 407 | 411 | 419 | 420 | 424 | 431 | 439 | 440 | 444 | 451 | 459 | 460 | 468 | 476 | 485 | 494 | 503 | 512 | 521 | 530 | 538 | 546 | 550 | 558 | 559 | 567 | 575 | 579 | 586 | 594 | 595 | 598 | 605 | 612 | 620 | 621 | 625 | 632 | 640 | 641 | 645 | 652 | 660 | 661 | 662 | 663 | -------------------------------------------------------------------------------- /_templates/tabbar-breeze.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 18 | 37 | 48 | 49 | 51 | 85 | 86 | 88 | 89 | 91 | image/svg+xml 92 | 94 | 95 | 96 | 97 | 98 | 102 | 109 | 113 | 121 | 122 | 126 | 133 | 141 | 142 | 146 | 153 | 161 | 162 | 170 | 178 | 186 | 194 | 202 | 210 | 217 | 224 | 231 | 238 | 247 | 256 | 265 | 274 | 283 | 292 | 300 | 308 | 312 | 320 | 321 | 329 | 337 | 346 | 355 | 364 | 373 | 382 | 391 | 399 | 407 | 411 | 419 | 420 | 424 | 431 | 439 | 440 | 444 | 451 | 459 | 460 | 468 | 476 | 485 | 494 | 503 | 512 | 521 | 530 | 538 | 546 | 550 | 558 | 559 | 567 | 575 | 579 | 586 | 594 | 595 | 598 | 605 | 612 | 620 | 621 | 625 | 632 | 640 | 641 | 645 | 652 | 660 | 661 | 662 | 663 | -------------------------------------------------------------------------------- /_templates/dialogs/background.svg: -------------------------------------------------------------------------------- 1 | 13 | 14 | 60 | 61 | 66 | 71 | 72 | 73 | 78 | 83 | 84 | 94 | 105 | 115 | 126 | 136 | 146 | 157 | 168 | 169 | 196 | 206 | 207 | 208 | 209 | 210 | image/svg+xml 211 | 212 | 213 | 214 | 215 | 216 | 217 | 218 | 226 | 234 | {{noDialogPadding}} 235 | 236 | 237 | 238 | 239 | 240 | 246 | 247 | 248 | 257 | 258 | 259 | 265 | 266 | 267 | 276 | 277 | 278 | 287 | 288 | 289 | 298 | 299 | 300 | 306 | 307 | 308 | 317 | 318 | 319 | 325 | 326 | 327 | 328 | 336 | 344 | 352 | 360 | 361 | 362 | 370 | 378 | 386 | 394 | 395 | 396 | 397 | 398 | 399 | 407 | 412 | 420 | 421 | 422 | 430 | 438 | 439 | 440 | 448 | 453 | 461 | 462 | 463 | 471 | 479 | 480 | 481 | 489 | 490 | 491 | 499 | 507 | 508 | 509 | 517 | 522 | 530 | 531 | 532 | 540 | 548 | 549 | 550 | 558 | 563 | 571 | 572 | 573 | 574 | 582 | 590 | 598 | 606 | 607 | 608 | 616 | 624 | 632 | 640 | 641 | 642 | 643 | 644 | 645 | 650 | 651 | 652 | 660 | 661 | 662 | 667 | 668 | 669 | 677 | 678 | 679 | 687 | 688 | 689 | 697 | 698 | 699 | 704 | 705 | 706 | 714 | 715 | 716 | 721 | 722 | 723 | 724 | 725 | -------------------------------------------------------------------------------- /desktoptheme.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | 3 | import configparser # Python 3 only 4 | import sys, os 5 | import glob 6 | import time 7 | import shutil 8 | import re 9 | from pprint import pprint 10 | 11 | 12 | class Point: 13 | def __init__(self, x, y): 14 | self.x = x 15 | self.y = y 16 | 17 | def __str__(self): 18 | return '{},{}'.format(self.x, self.y) 19 | 20 | 21 | # KDE rc files differences: 22 | # Keys are cAsE sensitive 23 | # No spaces around the = 24 | # [Sections can have spaces and : colons] 25 | # Parses [Sub][Sections] as "Sub][Sections", but cannot have comments on the [Section] line 26 | class KdeConfig(configparser.ConfigParser): 27 | def __init__(self, filename): 28 | super().__init__() 29 | 30 | # Keep case sensitive keys 31 | # http://stackoverflow.com/questions/19359556/configparser-reads-capital-keys-and-make-them-lower-case 32 | self.optionxform = str 33 | 34 | # Parse SubSections as "Sub][Sections" 35 | self.SECTCRE = re.compile(r"\[(?P
.+?)]\w*$") 36 | 37 | self.filename = filename 38 | self.read(self.filename) 39 | 40 | def set(self, section, option, value): 41 | if not self.has_section(section): 42 | self.add_section(section) 43 | super().set(section, option, str(value)) 44 | 45 | def setProp(self, key, value): 46 | section, option = key.split('.', 1) 47 | return self.set(section, option, value) 48 | 49 | def getProp(self, key): 50 | section, option = key.split('.', 1) 51 | return self.get(section, option) 52 | 53 | def default(self, section, option, value): 54 | if not self.has_option(section, option): 55 | self.set(section, option, value) 56 | 57 | def save(self): 58 | with open(self.filename, 'w') as fp: 59 | self.write(fp, space_around_delimiters=False) 60 | 61 | class KdeGlobalsConfig(KdeConfig): 62 | def __init__(self): 63 | super().__init__(os.path.abspath(os.path.expanduser('~/.config/kdeglobals'))) 64 | 65 | def limit(minVal, val, maxVal): 66 | return min(max(minVal, val), maxVal) 67 | 68 | def limitColor(val): 69 | return limit(0, val, 255) 70 | 71 | def deltaColor(colorStr, delta): 72 | r, g, b = map(int, colorStr.split(',')) 73 | if max(r, g, b) >= 128: 74 | d = -delta 75 | else: 76 | d = delta 77 | return '{},{},{}'.format(limitColor(r+d), limitColor(g+d), limitColor(b+d)) 78 | 79 | def hoverEffect(colorStr, rd, gd, bd): 80 | def f(a, b): 81 | if a + b > 255: 82 | return limitColor(a - b) 83 | else: 84 | return limitColor(a + b) 85 | r, g, b = map(int, colorStr.split(',')) 86 | return '{},{},{}'.format(f(r, rd), f(g, gd), f(b, bd)) 87 | 88 | def setTitlebarColors(backgroundColor='0,0,0', textColor='255,255,255'): 89 | filename = os.path.abspath(os.path.expanduser('~/.config/kdeglobals')) 90 | kdeglobals = KdeConfig(filename) 91 | kdeglobals['WM']['activeBackground'] = backgroundColor 92 | kdeglobals['WM']['inactiveBackground'] = backgroundColor 93 | kdeglobals['WM']['frame'] = backgroundColor 94 | kdeglobals['WM']['inactiveFrame'] = backgroundColor 95 | kdeglobals['WM']['activeForeground'] = textColor 96 | kdeglobals['WM']['inactiveForeground'] = textColor 97 | kdeglobals.save() 98 | 99 | def applyColorSchemeTitlebarColors(kdeglobals, colorScheme): 100 | # Default to Breeze.colors if the file leaves it undefined. 101 | activeBackgroundColor = colorScheme['Colors:Window'].get('BackgroundNormal', '239,240,241') 102 | kdeglobals['WM']['activeBackground'] = colorScheme['WM'].get('activeBackground', '71,80,87') 103 | kdeglobals['WM']['inactiveBackground'] = colorScheme['WM'].get('inactiveBackground', '239,240,241') 104 | kdeglobals['WM']['frame'] = colorScheme['WM'].get('frame', activeBackgroundColor) 105 | kdeglobals['WM']['inactiveFrame'] = colorScheme['WM'].get('inactiveFrame', kdeglobals['WM']['frame']) 106 | kdeglobals['WM']['activeForeground'] = colorScheme['WM'].get('activeForeground', '252,252,252') 107 | kdeglobals['WM']['inactiveForeground'] = colorScheme['WM'].get('inactiveForeground', '189,195,199') 108 | kdeglobals.save() 109 | 110 | 111 | 112 | def scanForColorScheme(colorSchemeName): 113 | def scanColorSchemeDir(colorSchemeDir): 114 | colorSchemeFilename = colorSchemeName + '.colors' 115 | dirFilenames = os.listdir(colorSchemeDir) 116 | 117 | if colorSchemeFilename in dirFilenames: 118 | colorSchemeFilepath = os.path.join(colorSchemeDir, colorSchemeFilename) 119 | # print(colorSchemeFilepath) 120 | colorScheme = KdeConfig(colorSchemeFilepath) 121 | return colorScheme 122 | 123 | # Color scheme was not found in this dir 124 | return None 125 | 126 | 127 | # 1st: Check the home color-scheme dir (downloads by the user). 128 | homeColorSchemeFilepath = os.path.abspath(os.path.expanduser('~/.local/share/color-schemes/')) 129 | colorScheme = scanColorSchemeDir(homeColorSchemeFilepath) 130 | if colorScheme: 131 | return colorScheme 132 | 133 | # 2nd: Check the root color-scheme dir. 134 | colorScheme = scanColorSchemeDir('/usr/share/color-schemes/') 135 | if colorScheme: 136 | return colorScheme 137 | 138 | # No color scheme fulfulled the conditions anywhere 139 | return None 140 | 141 | def resetTitlebarColors(): 142 | kdeglobals = KdeGlobalsConfig() 143 | 144 | colorSchemeName = kdeglobals['General'].get('ColorScheme', 'Breeze') 145 | 146 | colorScheme = scanForColorScheme(colorSchemeName) 147 | if colorScheme: 148 | applyColorSchemeTitlebarColors(kdeglobals, colorScheme) 149 | 150 | # elif os.path.isfile(localDesktopThemeFilename): 151 | # elif os.path.isfile(rootDesktopThemeFilename): 152 | # The color scheme is probably in a desktop theme folder... 153 | # Whatever, user can reapply it. 154 | else: 155 | # Deleting the keys will use white + highlight color, 156 | # which isn't the same as the default Breeze colors. 157 | del kdeglobals['WM']['activeBackground'] 158 | del kdeglobals['WM']['inactiveBackground'] 159 | del kdeglobals['WM']['frame'] 160 | del kdeglobals['WM']['inactiveFrame'] 161 | del kdeglobals['WM']['activeForeground'] 162 | del kdeglobals['WM']['inactiveForeground'] 163 | kdeglobals.save() 164 | 165 | class DesktopTheme: 166 | def __init__(self, themeName): 167 | self.themeName = themeName 168 | themeDir = os.path.abspath(os.path.expanduser('~/.local/share/plasma/desktoptheme')) 169 | self.themeDir = os.path.join(themeDir, themeName) 170 | self.dontReload = False 171 | 172 | def colorsConfig(self): 173 | filename = os.path.join(self.themeDir, 'colors') 174 | return KdeConfig(filename) 175 | 176 | def reloadTheme(self): 177 | if self.dontReload: 178 | # We're setting multiple properties at once, 179 | # and want to skip automatically reloading 180 | # after each change. 181 | return 182 | 183 | try: 184 | import dbus 185 | bus = dbus.SessionBus() 186 | obj = bus.get_object('org.kde.plasmashell', '/PlasmaShell') 187 | obj.evaluateScript('theme="breeze-dark";theme="' + self.themeName + '"') 188 | except: 189 | # Switch to another theme and back to apply the changes to breeze-alphablack. 190 | filename = os.path.abspath(os.path.expanduser('~/.config/plasmarc')) 191 | plasmarc = KdeConfig(filename) 192 | 193 | # Make sure we're not already in the process of changing the theme. 194 | if plasmarc['Theme']['name'] == self.themeName: 195 | plasmarc['Theme']['name'] = 'breeze-dark' 196 | plasmarc.save() 197 | time.sleep(1) 198 | plasmarc['Theme']['name'] = self.themeName 199 | plasmarc.save() 200 | 201 | def clearCache(self): 202 | wildcardPath='plasma-svgelements-{}_*'.format(self.themeName) 203 | cacheDir = os.path.realpath(os.path.expanduser('~/.cache/')) 204 | for file in glob.glob(os.path.join(cacheDir, wildcardPath)): 205 | print('Deleting cached theme file "{}"'.format(file)) 206 | os.remove(file) 207 | 208 | wildcardPath='plasma_theme_{}_*.kcache'.format(self.themeName) 209 | for file in glob.glob(os.path.join(cacheDir, wildcardPath)): 210 | print('Deleting cached theme file "{}"'.format(file)) 211 | os.remove(file) 212 | 213 | 214 | 215 | class BreezeAlphaBlack(DesktopTheme): 216 | def __init__(self, themeName='breeze-alphablack'): 217 | super().__init__(themeName=themeName) 218 | self.configProps = [ 219 | # (group, key, defaultValue, setterKey) 220 | ('dialog', 'opacity', 0.9, 'setDialogOpacity'), 221 | ('dialog', 'padding', 6, 'setDialogPadding'), 222 | ('dialog', 'radius', 3, 'setDialogRadius'), 223 | ('panel', 'opacity', 0.9, 'setPanelOpacity'), 224 | ('panel', 'padding', 2, 'setPanelPadding'), 225 | ('panel', 'radius', 3, 'setPanelRadius'), 226 | ('panel', 'taskStyle', 'inside', 'setTasksSvg'), 227 | ('panel', 'thickpadding', 8, 'setPanelThickPadding'), 228 | ('theme', 'accentColor', '0,0,0', 'setAccentColor'), 229 | ('theme', 'highlightColor', '61,174,230', 'setHighlightColor'), 230 | ('theme', 'textColor', '239,240,241', 'setTextColor'), 231 | ('widget', 'opacity', 0.9, 'setWidgetOpacity'), 232 | ] 233 | 234 | def themeConfig(self): 235 | filename = os.path.join(self.themeDir, 'config.ini') 236 | config = KdeConfig(filename) 237 | for (group, key, value, setterKey) in self.configProps: 238 | config.default(group, key, value) 239 | return config 240 | 241 | def configGet(self, propPath): 242 | config = self.themeConfig() 243 | value = config.getProp(propPath) 244 | return value 245 | 246 | def configSet(self, propPath, value): 247 | group1, key1 = propPath.split('.') 248 | for (group2, key2, defaultValue, setterKey) in self.configProps: 249 | if group1 == group2 and key1 == key2: 250 | setter = getattr(self, setterKey) 251 | setter(value) 252 | break 253 | 254 | def useTemplate(self, inPath, outPath): 255 | templatePath = os.path.join(self.themeDir, '_templates', inPath) 256 | outPath = os.path.join(self.themeDir, outPath) 257 | shutil.copyfile(templatePath, outPath) 258 | self.reloadTheme() 259 | 260 | def renderTemplate(self, templatePath, **kwargs): 261 | inFilename = os.path.join(self.themeDir, '_templates', templatePath) 262 | outFilename = os.path.join(self.themeDir, templatePath) 263 | outDir = os.path.dirname(outFilename) 264 | os.makedirs(outDir, exist_ok=True) 265 | with open(inFilename, 'r') as fin: 266 | with open(outFilename, 'w') as fout: 267 | for line in fin: 268 | for a, b in kwargs.items(): 269 | line = line.replace(a, b) 270 | fout.write(line) 271 | 272 | def getNonZero(self, config, section, key): 273 | value = float(config.get(section, key)) 274 | value = max(0.001, value) 275 | return value 276 | 277 | def getDialogPadding(self, config, section='dialog'): 278 | # Plasma doesn't like a padding of 0, so just use a really small number (which is rounded to 0). 279 | return self.getNonZero(config, section, 'padding') 280 | 281 | def getDialogRadius(self, config, section='dialog'): 282 | # It'd be a pain to adjust the paths to be perfectly square edge, so just do a really small radius. 283 | return self.getNonZero(config, section, 'radius') 284 | 285 | def getPanelThickPadding(self, config, section='panel'): 286 | if section == 'dialog': 287 | # Is not actually implemented. 288 | return 0 289 | else: 290 | # Note: The Panel.qml code in the desktop shell package defines an upper limit for 291 | # the thick margins. The panel must fit a smallMedium (22px) icon. 292 | # * https://invent.kde.org/plasma/plasma-desktop/-/blob/master/desktoppackage/contents/views/Panel.qml 293 | # * https://invent.kde.org/frameworks/kiconthemes/blob/master/src/kiconloader.h#L149 294 | # * /usr/share/plasma/shells/org.kde.plasma.desktop/contents/views/Panel.qml 295 | # So a 24px panel will have a maximum padding of (24-22)/2 = 1px 296 | # 30px panel will have a maximum padding of (30-22)/2 = 4px 297 | # 38px panel will have a maximum padding of (38-22)/2 = 8px 298 | # 64px panel will have a maximum padding of (64-22)/2 = 21px 299 | # So a panel needs to be 38px tall to actually have a thick padding of 8px (Breeze's default). 300 | return self.getNonZero(config, section, 'thickpadding') 301 | 302 | def calcCorners(self, padding, thickPadding, radius): 303 | out = {} 304 | out['{{padding}}'] = str(padding) 305 | out['{{thickPadding}}'] = str(thickPadding) 306 | 307 | centerSize = 32 308 | # radius (Default = 3) 309 | size = radius # max(6, radius) # Default = 6 310 | extra = size - radius # Default = 3 311 | farEdgeOffset = size + centerSize # Default = 38 312 | marginThickness = 4 # This is just a visual cue 313 | marginCenterOffset = size + (centerSize/2) - (marginThickness/2) # Default = 20 314 | marginFarOffset = size + centerSize + size - padding 315 | if thickPadding > 0: 316 | marginCenterOffset -= (marginThickness/2) 317 | thickMarginCenterOffset = marginCenterOffset + marginThickness 318 | thickMarginFarOffset = size + centerSize + size - thickPadding 319 | 320 | out['{{centerSize}}'] = str(centerSize) 321 | out['{{edgeSize}}'] = str(size) 322 | out['{{extraSize}}'] = str(extra) 323 | out['{{farEdgeOffset}}'] = str(farEdgeOffset) 324 | out['{{marginThickness}}'] = str(marginThickness) 325 | out['{{marginCenterOffset}}'] = str(marginCenterOffset) 326 | out['{{marginFarOffset}}'] = str(marginFarOffset) 327 | out['{{thickMarginCenterOffset}}'] = str(thickMarginCenterOffset) 328 | out['{{thickMarginFarOffset}}'] = str(thickMarginFarOffset) 329 | 330 | third = radius * 1/3 331 | twoThird = radius * 2/3 332 | 333 | #--- Background+Mask Tileset 334 | # topLeft (Clockwise) 335 | # m 6,6 h -6 v -3 c 0,-2 1,-3 3,-3 h 3 z 336 | path = 'm {} h {} v {} c {} {} {} h {} z'.format( 337 | # m 338 | Point(size, size), # point touching center 339 | # h 340 | -size, 341 | # v 342 | -extra, 343 | # c 344 | Point(0, -twoThird), 345 | Point(third, -radius), 346 | Point(radius, -radius), 347 | # h 348 | extra, 349 | # z 350 | ) 351 | out['{{topLeftPath}}'] = path 352 | 353 | # topRight (Clockwise) 354 | # m 38,6 v -6 h 3 c 2,0 3,1 3,3 v 3 z 355 | path = 'm {} v {} h {} c {} {} {} v {} z'.format( 356 | # m 357 | Point(farEdgeOffset, size), # point touching center 358 | # v 359 | -size, 360 | # h 361 | extra, 362 | # c 363 | Point(twoThird, 0), 364 | Point(radius, third), 365 | Point(radius, radius), 366 | # v 367 | extra, 368 | # z 369 | ) 370 | out['{{topRightPath}}'] = path 371 | 372 | # bottomLeft (Clockwise) 373 | # m 6,38 v 6 h -3 c -2,0 -3,-1 -3,-3 v -3 z 374 | path = 'm {} v {} h {} c {} {} {} v {} z'.format( 375 | # m 376 | Point(size, farEdgeOffset), # point touching center 377 | # v 378 | size, 379 | # h 380 | -extra, 381 | # c 382 | Point(-twoThird, 0), 383 | Point(-radius, -third), 384 | Point(-radius, -radius), 385 | # v 386 | -extra, 387 | # z 388 | ) 389 | out['{{bottomLeftPath}}'] = path 390 | 391 | # bottomRight (Clockwise) 392 | # m 38,38 h 6 v 3 c 0,2 -1,3 -3,3 h -3 z 393 | path = 'm {} h {} v {} c {} {} {} h {} z'.format( 394 | # m 395 | Point(farEdgeOffset, farEdgeOffset), # point touching center 396 | # h 397 | size, 398 | # v 399 | extra, 400 | # c 401 | Point(0, twoThird), 402 | Point(-third, radius), 403 | Point(-radius, radius), 404 | # h 405 | -extra, 406 | # z 407 | ) 408 | out['{{bottomRightPath}}'] = path 409 | 410 | #--- Shadow Tileset 411 | shadowSize = 6 412 | out['{{shadowSize}}'] = str(shadowSize) 413 | 414 | shadowAndRadius = shadowSize + radius # Default = 9 415 | shadowEdgeSize = shadowSize + size # Default = 12 416 | shadowFarEdge = shadowEdgeSize + centerSize # Default = 44 417 | shadowFarShadow = shadowEdgeSize + centerSize + size # Default = 50 418 | shadowTilesetSize = shadowEdgeSize + centerSize + shadowEdgeSize # Default = 56 419 | out['{{shadowAndRadius}}'] = str(shadowAndRadius) 420 | out['{{shadowEdgeSize}}'] = str(shadowEdgeSize) 421 | out['{{shadowFarEdge}}'] = str(shadowFarEdge) 422 | out['{{shadowFarShadow}}'] = str(shadowFarShadow) 423 | out['{{shadowTilesetSize}}'] = str(shadowTilesetSize) 424 | 425 | edgeStopDone = 2/3 # Default = 0.66 426 | cornerStopA = size / shadowEdgeSize # Default = 0.33 427 | cornerStopB = (size + shadowSize*edgeStopDone) / shadowEdgeSize # Default = 0.77 428 | out['{{edgeStopDone}}'] = str(edgeStopDone) 429 | out['{{cornerStopA}}'] = str(cornerStopA) 430 | out['{{cornerStopB}}'] = str(cornerStopB) 431 | 432 | # shadowTopLeft (CounterClockwise) 433 | # m 0,0 v 9 h 6 c 0,-2 1,-3 3,-3 v -6 z 434 | path = 'm {} v {} h {} c {} {} {} v {} z'.format( 435 | # m 436 | Point(0, 0), # point furthest from center 437 | # v 438 | shadowAndRadius, 439 | # h 440 | shadowSize, 441 | # c 442 | Point(0, -twoThird), 443 | Point(third, -radius), 444 | Point(radius, -radius), 445 | # v 446 | -shadowSize, 447 | # z 448 | ) 449 | out['{{shadowTopLeftPath}}'] = path 450 | 451 | # shadowTopRight (CounterClockwise) 452 | # m 56,0 h -9 v 6 c 2,0 3,1 3,3 h 6 z 453 | path = 'm {} h {} v {} c {} {} {} h {} z'.format( 454 | # m 455 | Point(shadowTilesetSize, 0), # point furthest from center 456 | # h 457 | -shadowAndRadius, 458 | # v 459 | shadowSize, 460 | # c 461 | Point(twoThird, 0), 462 | Point(radius, third), 463 | Point(radius, radius), 464 | # h 465 | shadowSize, 466 | # z 467 | ) 468 | out['{{shadowTopRightPath}}'] = path 469 | 470 | # shadowBottomLeft (CounterClockwise) 471 | # m 0,56 h 9 v -6 c -2,0 -3,-1 -3,-3 h -6 z 472 | path = 'm {} h {} v {} c {} {} {} h {} z'.format( 473 | # m 474 | Point(0, shadowTilesetSize), # point furthest from center 475 | # h 476 | shadowAndRadius, 477 | # v 478 | -shadowSize, 479 | # c 480 | Point(-twoThird, 0), 481 | Point(-radius, -third), 482 | Point(-radius, -radius), 483 | # h 484 | -shadowSize, 485 | # z 486 | ) 487 | out['{{shadowBottomLeftPath}}'] = path 488 | 489 | # shadowBottomRight (CounterClockwise) 490 | # m 56,56 v -9 h -6 c 0,2 -1,3 -3,3 v 6 z 491 | path = 'm {} v {} h {} c {} {} {} v {} z'.format( 492 | # m 493 | Point(shadowTilesetSize, shadowTilesetSize), # point furthest from center 494 | # v 495 | -shadowAndRadius, 496 | # h 497 | -shadowSize, 498 | # c 499 | Point(0, twoThird), 500 | Point(-third, radius), 501 | Point(-radius, radius), 502 | # v 503 | shadowSize, 504 | # z 505 | ) 506 | out['{{shadowBottomRightPath}}'] = path 507 | 508 | return out 509 | 510 | def getDialogVars(self, config, section='dialog'): 511 | dialogPadding = config.get(section, 'padding') 512 | noDialogPadding = config.getint(section, 'padding') == 0 513 | fillOpacity = config.get(section, 'opacity') 514 | if section == 'panel': 515 | if float(fillOpacity) >= 0.3: 516 | shadowOpacity = 1 517 | else: 518 | # Background fill isn't strong enough, hide shadows. 519 | shadowOpacity = 0 520 | else: 521 | # Dialogs should always have shadows. 522 | shadowOpacity = 1 523 | 524 | dialogVars = { 525 | '{{fillOpacity}}': str(fillOpacity), 526 | '{{shadowOpacity}}': str(shadowOpacity), 527 | '{{noDialogPadding}}': '' if noDialogPadding else '', 528 | } 529 | 530 | dialogPadding = self.getDialogPadding(config, section) 531 | dialogRadius = self.getDialogRadius(config, section) 532 | thickPadding = self.getPanelThickPadding(config, section) 533 | cornerVars = self.calcCorners(dialogPadding, thickPadding, dialogRadius) 534 | dialogVars.update(cornerVars) 535 | return dialogVars 536 | 537 | def renderDialogBackground(self, config): 538 | dialogVars = self.getDialogVars(config) 539 | pprint(dialogVars) 540 | self.renderTemplate('dialogs/background.svg', **dialogVars) 541 | 542 | def renderPlasmoidHeading(self, config): 543 | dialogVars = self.getDialogVars(config) 544 | self.renderTemplate('widgets/plasmoidheading.svg', **dialogVars) 545 | 546 | def setDialogProperty(self, key, newValue): 547 | config = self.themeConfig() 548 | config.set('dialog', key, newValue) 549 | self.renderDialogBackground(config) 550 | self.renderPlasmoidHeading(config) 551 | config.save() 552 | self.clearCache() # Not really necessary 553 | self.reloadTheme() 554 | 555 | def setDialogOpacity(self, newOpacity=0.9): 556 | self.setDialogProperty('opacity', newOpacity) 557 | 558 | def setDialogPadding(self, newPadding=6): 559 | self.setDialogProperty('padding', newPadding) 560 | 561 | def setDialogRadius(self, newRadius=3): 562 | self.setDialogProperty('radius', newRadius) 563 | 564 | def renderPanel(self, config): 565 | panelVars = self.getDialogVars(config, section='panel') 566 | pprint(panelVars) 567 | self.renderTemplate('widgets/panel-background.svg', **panelVars) 568 | 569 | def setPanelProperty(self, key, newValue): 570 | config = self.themeConfig() 571 | config.set('panel', key, newValue) 572 | self.renderPanel(config) 573 | config.save() 574 | self.clearCache() # Only necessary for setPanelPadding 575 | self.reloadTheme() 576 | 577 | def setPanelOpacity(self, newOpacity=0.9): 578 | self.setPanelProperty('opacity', newOpacity) 579 | 580 | def setPanelPadding(self, newPadding=2): 581 | self.setPanelProperty('padding', newPadding) 582 | 583 | def setPanelRadius(self, newRadius=3): 584 | self.setPanelProperty('radius', newRadius) 585 | 586 | def setPanelThickPadding(self, newPadding=8): 587 | self.setPanelProperty('thickpadding', newPadding) 588 | 589 | def renderWidgetBackground(self, config): 590 | # Breeze's widget/background.svg 591 | # fill: 0.9 592 | # shadows: 1.0 (Linear gradients) 593 | # corner shadows: 0.6 (Radial gradients) 594 | fillOpacity = float(config.getProp('widget.opacity')) 595 | if fillOpacity == 0: 596 | # Hide shadows completely if there's no fill 597 | shadowOpacity = 0 598 | cornerOpacity = 0 599 | else: 600 | shadowOpacity = min(fillOpacity + 0.1, 1) 601 | cornerOpacity = 0.6 * shadowOpacity 602 | 603 | self.renderTemplate('widgets/background.svg', **{ 604 | '{{fillOpacity}}': str(fillOpacity), 605 | '{{shadowOpacity}}': str(shadowOpacity), 606 | '{{cornerOpacity}}': str(cornerOpacity), 607 | }) 608 | 609 | def setWidgetOpacity(self, newOpacity=0.9): 610 | config = self.themeConfig() 611 | config.set('widget', 'opacity', newOpacity) 612 | self.renderWidgetBackground(config) 613 | config.save() 614 | self.clearCache() # Not really necessary 615 | self.reloadTheme() 616 | 617 | def _applyColors(self, accentColor='0,0,0', textColor='239,240,241', highlightColor='61,174,230'): 618 | altColor = deltaColor(accentColor, 23) 619 | compColor = deltaColor(accentColor, 17) 620 | compFocusColor = hoverEffect(highlightColor, -31, -28, 25) # 61,174,230 => 30,146,255 621 | focusColor = highlightColor 622 | hoverColor = highlightColor 623 | selectionColor = highlightColor 624 | selectionAltColor = hoverEffect(selectionColor, -13, -36, -47) # 61,174,230 => 48,138,183 625 | 626 | print('BackgroundNormal: {}'.format(accentColor)) 627 | print('BackgroundAlternate: {}'.format(altColor)) 628 | print('Complementary.BackgroundNormal: {}'.format(compColor)) 629 | print('Complementary.DecorationFocus: {}'.format(compFocusColor)) 630 | print('DecorationFocus: {}'.format(focusColor)) 631 | print('DecorationHover: {}'.format(hoverColor)) 632 | print('Selection.BackgroundNormal: {}'.format(selectionColor)) 633 | print('Selection.BackgroundAlternate: {}'.format(selectionAltColor)) 634 | 635 | setTitlebarColors(accentColor, textColor) 636 | 637 | colors = self.colorsConfig() 638 | allColorGroups = [ 639 | 'Colors:Button', 640 | 'Colors:Selection', 641 | 'Colors:Tooltip', 642 | 'Colors:View', 643 | 'Colors:Window', 644 | 'Colors:Complementary', 645 | 'Colors:Header', 646 | ] 647 | allButComplementaryGroup = [g for g in allColorGroups if g != 'Colors:Complementary'] 648 | def applyToGroups(groups, key, value): 649 | for group in groups: 650 | colors[group][key] = value 651 | 652 | colors['Colors:Button']['BackgroundNormal'] = compColor 653 | colors['Colors:Button']['BackgroundAlternate'] = altColor 654 | colors['Colors:Button']['ForegroundNormal'] = textColor 655 | colors['Colors:Header']['BackgroundNormal'] = accentColor 656 | colors['Colors:Header']['BackgroundAlternate'] = altColor 657 | colors['Colors:Header']['ForegroundNormal'] = textColor 658 | colors['Colors:View']['BackgroundNormal'] = accentColor 659 | colors['Colors:View']['BackgroundAlternate'] = altColor 660 | colors['Colors:View']['ForegroundNormal'] = textColor 661 | colors['Colors:Window']['BackgroundNormal'] = accentColor 662 | colors['Colors:Window']['BackgroundAlternate'] = altColor 663 | colors['Colors:Window']['ForegroundNormal'] = textColor 664 | colors['Colors:Complementary']['BackgroundNormal'] = compColor 665 | colors['Colors:Complementary']['BackgroundAlternate'] = altColor 666 | colors['Colors:Complementary']['ForegroundNormal'] = textColor 667 | 668 | # Focus 669 | applyToGroups(allButComplementaryGroup, 'DecorationFocus', focusColor) 670 | colors['Colors:Complementary']['DecorationFocus'] = compFocusColor 671 | 672 | # Hover 673 | applyToGroups(allColorGroups, 'DecorationHover', hoverColor) 674 | colors['Colors:Selection']['BackgroundNormal'] = selectionColor # Note this variable controls `theme.highlightColor` 675 | colors['Colors:Selection']['BackgroundAlternate'] = selectionAltColor 676 | 677 | colors.save() 678 | 679 | config = self.themeConfig() 680 | config.set('theme', 'accentColor', accentColor) 681 | config.set('theme', 'highlightColor', highlightColor) 682 | config.set('theme', 'textColor', textColor) 683 | config.save() 684 | 685 | self.reloadTheme() 686 | 687 | def applyColors(self, accentColor=None, textColor=None, highlightColor=None): 688 | config = self.themeConfig() 689 | if accentColor is None: 690 | accentColor = config.get('theme', 'accentColor') 691 | if textColor is None: 692 | textColor = config.get('theme', 'textColor') 693 | if highlightColor is None: 694 | highlightColor = config.get('theme', 'highlightColor') 695 | self._applyColors(accentColor, textColor, highlightColor) 696 | 697 | def setAccentColor(self, accentColor): 698 | self.applyColors(accentColor=accentColor) 699 | 700 | def setHighlightColor(self, highlightColor): 701 | self.applyColors(highlightColor=highlightColor) 702 | 703 | def setTextColor(self, textColor): 704 | self.applyColors(textColor=textColor) 705 | 706 | def setTasksSvg(self, taskTheme): 707 | templatePath = "tasks-{}.svg".format(taskTheme) 708 | self.useTemplate(templatePath, 'widgets/tasks.svg') 709 | 710 | config = self.themeConfig() 711 | config.set('panel', 'taskStyle', taskTheme) 712 | config.save() 713 | 714 | self.reloadTheme() 715 | 716 | def resetToDefaults(self): 717 | self.dontReload = True 718 | for (group, key, value, setterKey) in self.configProps: 719 | print("===[ {}.{}: {} ]===".format(group, key, value)) 720 | setter = getattr(self, setterKey) 721 | setter(value) 722 | 723 | self.dontReload = False 724 | self.reloadTheme() 725 | 726 | 727 | 728 | 729 | #--- Main 730 | def theme_getall(args): 731 | import json 732 | 733 | desktoptheme = BreezeAlphaBlack() 734 | config = desktoptheme.themeConfig() 735 | 736 | if args.json: 737 | out = {} 738 | for section in config.sections(): 739 | out[section] = {} 740 | for name, value in config.items(section): 741 | out[section][name] = value 742 | print(json.dumps(out, indent="\t")) 743 | else: 744 | for section in config.sections(): 745 | for name, value in config.items(section): 746 | print("{}.{}: {}".format(section, name, value)) 747 | 748 | def theme_get(args): 749 | desktopTheme = BreezeAlphaBlack() 750 | argsVars = vars(args) 751 | propPath = argsVars['section.property'] 752 | value = desktopTheme.configGet(propPath) 753 | print(value) 754 | 755 | def theme_set(args): 756 | desktopTheme = BreezeAlphaBlack() 757 | config = desktopTheme.themeConfig() 758 | argsVars = vars(args) 759 | propPath = argsVars['section.property'] 760 | desktopTheme.configSet(propPath, args.value) 761 | 762 | 763 | def theme_setTitlebarColors(args): 764 | def parseColorStr(colorStr): 765 | r, g, b = map(int, colorStr.split(',')) 766 | return "{},{},{}".format(r, g, b) 767 | 768 | argsVars = vars(args) 769 | if argsVars.get('bgColor'): 770 | bgColor = parseColorStr(argsVars.get('bgColor')) 771 | if argsVars.get('textColor'): 772 | textColor = parseColorStr(argsVars.get('textColor')) 773 | setTitlebarColors(bgColor, textColor) 774 | else: 775 | setTitlebarColors(bgColor) 776 | 777 | def theme_reset(args): 778 | desktopTheme = BreezeAlphaBlack() 779 | desktopTheme.resetToDefaults() 780 | 781 | def theme_resetTitlebarColors(args): 782 | resetTitlebarColors() 783 | 784 | def main(): 785 | import argparse 786 | 787 | parser = argparse.ArgumentParser( 788 | prog='desktoptheme', 789 | description='Python script to modify a desktop theme.', 790 | epilog='Note that colors must be [red],[green],[blue] (seperated by commas). [red/green/blue] are integers from 0-255.' 791 | ) 792 | subparsers = parser.add_subparsers() 793 | 794 | def add_subcommand(name, func, *args, description=None): 795 | tokens = ['[{}]'.format(arg) for arg in args] 796 | tokens = ['python3', 'desktoptheme.py', name] + tokens 797 | cmdstr = ' '.join(tokens) 798 | helpstr = cmdstr 799 | if description: 800 | helpstr += ' - ' + description 801 | parser_subcommand = subparsers.add_parser(name, help=helpstr) 802 | parser_subcommand.set_defaults(func=func) 803 | for arg in args: 804 | parser_subcommand.add_argument(arg) 805 | return parser_subcommand 806 | 807 | parser_getall = add_subcommand('getall', theme_getall) 808 | parser_getall.add_argument('--json', default=False, action='store_true') 809 | add_subcommand('get', theme_get, 'section.property') 810 | add_subcommand('set', theme_set, 'section.property', 'value') 811 | add_subcommand('settitlebarcolor', theme_setTitlebarColors, 'bgColor', description='Eg: 0,0,0') 812 | add_subcommand('settitlebarcolors', theme_setTitlebarColors, 'bgColor', 'textColor', description='Eg: 0,0,0 255,255,255') 813 | add_subcommand('reset', theme_reset) 814 | add_subcommand('resettitlebarcolors', theme_resetTitlebarColors) 815 | 816 | args = parser.parse_args() 817 | 818 | if 'func' in args: 819 | try: 820 | args.func(args) 821 | except KeyboardInterrupt: 822 | pass 823 | else: 824 | parser.print_help() 825 | 826 | 827 | if __name__ == '__main__': 828 | main() 829 | --------------------------------------------------------------------------------