Merge pull request #3274 from fieldOfView/feature_setting_visibility_profiles

Setting Visibility Presets sidebar menu
This commit is contained in:
Lipu Fei 2018-03-14 13:25:59 +01:00 committed by GitHub
commit 984e6ddd08
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 376 additions and 185 deletions

View file

@ -91,6 +91,7 @@ from cura.Settings.UserChangesModel import UserChangesModel
from cura.Settings.ExtrudersModel import ExtrudersModel
from cura.Settings.MaterialSettingsVisibilityHandler import MaterialSettingsVisibilityHandler
from cura.Settings.ContainerManager import ContainerManager
from cura.Settings.SettingVisibilityPresetsModel import SettingVisibilityPresetsModel
from cura.ObjectsModel import ObjectsModel
@ -140,6 +141,7 @@ class CuraApplication(QtApplication):
MachineStack = Resources.UserType + 7
ExtruderStack = Resources.UserType + 8
DefinitionChangesContainer = Resources.UserType + 9
SettingVisibilityPreset = Resources.UserType + 10
Q_ENUMS(ResourceTypes)
@ -187,6 +189,7 @@ class CuraApplication(QtApplication):
Resources.addStorageType(self.ResourceTypes.ExtruderStack, "extruders")
Resources.addStorageType(self.ResourceTypes.MachineStack, "machine_instances")
Resources.addStorageType(self.ResourceTypes.DefinitionChangesContainer, "definition_changes")
Resources.addStorageType(self.ResourceTypes.SettingVisibilityPreset, "setting_visibility")
ContainerRegistry.getInstance().addResourceType(self.ResourceTypes.QualityInstanceContainer, "quality")
ContainerRegistry.getInstance().addResourceType(self.ResourceTypes.QualityInstanceContainer, "quality_changes")
@ -373,19 +376,9 @@ class CuraApplication(QtApplication):
preferences.setDefault("local_file/last_used_type", "text/x-gcode")
setting_visibily_preset_names = self.getVisibilitySettingPresetTypes()
preferences.setDefault("general/visible_settings_preset", setting_visibily_preset_names)
default_visibility_profile = SettingVisibilityPresetsModel.getInstance().getItem(0)
preset_setting_visibility_choice = Preferences.getInstance().getValue("general/preset_setting_visibility_choice")
default_preset_visibility_group_name = "Basic"
if preset_setting_visibility_choice == "" or preset_setting_visibility_choice is None:
if preset_setting_visibility_choice not in setting_visibily_preset_names:
preset_setting_visibility_choice = default_preset_visibility_group_name
visible_settings = self.getVisibilitySettingPreset(settings_preset_name = preset_setting_visibility_choice)
preferences.setDefault("general/visible_settings", visible_settings)
preferences.setDefault("general/preset_setting_visibility_choice", preset_setting_visibility_choice)
preferences.setDefault("general/visible_settings", ";".join(default_visibility_profile["settings"]))
self.applicationShuttingDown.connect(self.saveSettings)
self.engineCreatedSignal.connect(self._onEngineCreated)
@ -402,91 +395,6 @@ class CuraApplication(QtApplication):
CuraApplication.Created = True
@pyqtSlot(str, result = str)
def getVisibilitySettingPreset(self, settings_preset_name) -> str:
result = self._loadPresetSettingVisibilityGroup(settings_preset_name)
formatted_preset_settings = self._serializePresetSettingVisibilityData(result)
return formatted_preset_settings
## Serialise the given preset setting visibitlity group dictionary into a string which is concatenated by ";"
#
def _serializePresetSettingVisibilityData(self, settings_data: dict) -> str:
result_string = ""
for key in settings_data:
result_string += key + ";"
for value in settings_data[key]:
result_string += value + ";"
return result_string
## Load the preset setting visibility group with the given name
#
def _loadPresetSettingVisibilityGroup(self, visibility_preset_name) -> Dict[str, str]:
preset_dir = Resources.getPath(Resources.PresetSettingVisibilityGroups)
result = {}
right_preset_found = False
for item in os.listdir(preset_dir):
file_path = os.path.join(preset_dir, item)
if not os.path.isfile(file_path):
continue
parser = ConfigParser(allow_no_value = True) # accept options without any value,
try:
parser.read([file_path])
if not parser.has_option("general", "name"):
continue
if parser["general"]["name"] == visibility_preset_name:
right_preset_found = True
for section in parser.sections():
if section == 'general':
continue
else:
section_settings = []
for option in parser[section].keys():
section_settings.append(option)
result[section] = section_settings
if right_preset_found:
break
except Exception as e:
Logger.log("e", "Failed to load setting visibility preset %s: %s", file_path, str(e))
return result
## Check visibility setting preset folder and returns available types
#
def getVisibilitySettingPresetTypes(self):
preset_dir = Resources.getPath(Resources.PresetSettingVisibilityGroups)
result = {}
for item in os.listdir(preset_dir):
file_path = os.path.join(preset_dir, item)
if not os.path.isfile(file_path):
continue
parser = ConfigParser(allow_no_value=True) # accept options without any value,
try:
parser.read([file_path])
if not parser.has_option("general", "name") and not parser.has_option("general", "weight"):
continue
result[parser["general"]["weight"]] = parser["general"]["name"]
except Exception as e:
Logger.log("e", "Failed to load setting preset %s: %s", file_path, str(e))
return result
def _onEngineCreated(self):
self._engine.addImageProvider("camera", CameraImageProvider.CameraImageProvider())
@ -986,6 +894,7 @@ class CuraApplication(QtApplication):
qmlRegisterType(MachineNameValidator, "Cura", 1, 0, "MachineNameValidator")
qmlRegisterType(UserChangesModel, "Cura", 1, 0, "UserChangesModel")
qmlRegisterSingletonType(ContainerManager, "Cura", 1, 0, "ContainerManager", ContainerManager.createContainerManager)
qmlRegisterSingletonType(SettingVisibilityPresetsModel, "Cura", 1, 0, "SettingVisibilityPresetsModel", SettingVisibilityPresetsModel.createSettingVisibilityPresetsModel)
# As of Qt5.7, it is necessary to get rid of any ".." in the path for the singleton to work.
actions_url = QUrl.fromLocalFile(os.path.abspath(Resources.getPath(CuraApplication.ResourceTypes.QmlFiles, "Actions.qml")))

View file

@ -0,0 +1,136 @@
# Copyright (c) 2018 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
import os
import urllib
from configparser import ConfigParser
from PyQt5.QtCore import pyqtProperty, Qt, pyqtSignal, pyqtSlot, QUrl
from UM.Logger import Logger
from UM.Qt.ListModel import ListModel
from UM.Preferences import Preferences
from UM.Resources import Resources
from UM.MimeTypeDatabase import MimeTypeDatabase, MimeTypeNotFoundError
import cura.CuraApplication
class SettingVisibilityPresetsModel(ListModel):
IdRole = Qt.UserRole + 1
NameRole = Qt.UserRole + 2
SettingsRole = Qt.UserRole + 4
def __init__(self, parent = None):
super().__init__(parent)
self.addRoleName(self.IdRole, "id")
self.addRoleName(self.NameRole, "name")
self.addRoleName(self.SettingsRole, "settings")
self._populate()
self._preferences = Preferences.getInstance()
self._preferences.addPreference("cura/active_setting_visibility_preset", "custom") # Preference to store which preset is currently selected
self._preferences.addPreference("cura/custom_visible_settings", "") # Preference that stores the "custom" set so it can always be restored (even after a restart)
self._preferences.preferenceChanged.connect(self._onPreferencesChanged)
self._active_preset = self._preferences.getValue("cura/active_setting_visibility_preset")
if self.find("id", self._active_preset) < 0:
self._active_preset = "custom"
self.activePresetChanged.emit()
def _populate(self):
items = []
for item in Resources.getAllResourcesOfType(cura.CuraApplication.CuraApplication.ResourceTypes.SettingVisibilityPreset):
try:
mime_type = MimeTypeDatabase.getMimeTypeForFile(item)
except MimeTypeNotFoundError:
Logger.log("e", "Could not determine mime type of file %s", item)
continue
id = urllib.parse.unquote_plus(mime_type.stripExtension(os.path.basename(item)))
if not os.path.isfile(item):
continue
parser = ConfigParser(allow_no_value=True) # accept options without any value,
try:
parser.read([item])
if not parser.has_option("general", "name") and not parser.has_option("general", "weight"):
continue
settings = []
for section in parser.sections():
if section == 'general':
continue
settings.append(section)
for option in parser[section].keys():
settings.append(option)
items.append({
"id": id,
"name": parser["general"]["name"],
"weight": parser["general"]["weight"],
"settings": settings
})
except Exception as e:
Logger.log("e", "Failed to load setting preset %s: %s", file_path, str(e))
items.sort(key = lambda k: (k["weight"], k["id"]))
self.setItems(items)
@pyqtSlot(str)
def setActivePreset(self, preset_id):
if preset_id != "custom" and self.find("id", preset_id) == -1:
Logger.log("w", "Tried to set active preset to unknown id %s", preset_id)
return
if preset_id == "custom" and self._active_preset == "custom":
# Copy current visibility set to custom visibility set preference so it can be restored later
visibility_string = self._preferences.getValue("general/visible_settings")
self._preferences.setValue("cura/custom_visible_settings", visibility_string)
self._preferences.setValue("cura/active_setting_visibility_preset", preset_id)
self._active_preset = preset_id
self.activePresetChanged.emit()
activePresetChanged = pyqtSignal()
@pyqtProperty(str, notify = activePresetChanged)
def activePreset(self):
return self._active_preset
def _onPreferencesChanged(self, name):
if name != "general/visible_settings":
return
if self._active_preset != "custom":
return
# Copy current visibility set to custom visibility set preference so it can be restored later
visibility_string = self._preferences.getValue("general/visible_settings")
self._preferences.setValue("cura/custom_visible_settings", visibility_string)
# Factory function, used by QML
@staticmethod
def createSettingVisibilityPresetsModel(engine, js_engine):
return SettingVisibilityPresetsModel.getInstance()
## Get the singleton instance for this class.
@classmethod
def getInstance(cls) -> "SettingVisibilityPresetsModel":
# Note: Explicit use of class name to prevent issues with inheritance.
if not SettingVisibilityPresetsModel.__instance:
SettingVisibilityPresetsModel.__instance = cls()
return SettingVisibilityPresetsModel.__instance
__instance = None # type: "SettingVisibilityPresetsModel"

View file

@ -594,7 +594,7 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
Logger.log("w", "Workspace did not contain visible settings. Leaving visibility unchanged")
else:
global_preferences.setValue("general/visible_settings", visible_settings)
global_preferences.setValue("general/preset_setting_visibility_choice", "Custom")
global_preferences.setValue("cura/active_setting_visibility_preset", "custom")
categories_expanded = temp_preferences.getValue("cura/categories_expanded")
if categories_expanded is None:

View file

@ -653,7 +653,10 @@ UM.MainWindow
{
preferences.visible = true;
preferences.setPage(1);
preferences.getCurrentItem().scrollToSection(source.key);
if(source && source.key)
{
preferences.getCurrentItem().scrollToSection(source.key);
}
}
}

View file

@ -0,0 +1,82 @@
// Copyright (c) 2018 Ultimaker B.V.
// Cura is released under the terms of the LGPLv3 or higher.
import QtQuick 2.2
import QtQuick.Controls 1.1
import UM 1.2 as UM
import Cura 1.0 as Cura
Menu
{
id: menu
title: catalog.i18nc("@action:inmenu", "Visible Settings")
property bool showingSearchResults
property bool showingAllSettings
signal showAllSettings()
signal showSettingVisibilityProfile()
MenuItem
{
text: catalog.i18nc("@action:inmenu", "Custom selection")
checkable: true
checked: !showingSearchResults && !showingAllSettings && Cura.SettingVisibilityPresetsModel.activePreset == "custom"
exclusiveGroup: group
onTriggered:
{
Cura.SettingVisibilityPresetsModel.setActivePreset("custom");
// Restore custom set from preference
UM.Preferences.setValue("general/visible_settings", UM.Preferences.getValue("cura/custom_visible_settings"));
showSettingVisibilityProfile();
}
}
MenuSeparator { }
Instantiator
{
model: Cura.SettingVisibilityPresetsModel
MenuItem
{
text: model.name
checkable: true
checked: model.id == Cura.SettingVisibilityPresetsModel.activePreset
exclusiveGroup: group
onTriggered:
{
Cura.SettingVisibilityPresetsModel.setActivePreset(model.id);
UM.Preferences.setValue("general/visible_settings", model.settings.join(";"));
showSettingVisibilityProfile();
}
}
onObjectAdded: menu.insertItem(index, object)
onObjectRemoved: menu.removeItem(object)
}
MenuSeparator {}
MenuItem
{
text: catalog.i18nc("@action:inmenu", "All Settings")
checkable: true
checked: showingAllSettings
exclusiveGroup: group
onTriggered:
{
showAllSettings();
}
}
MenuSeparator {}
MenuItem
{
text: catalog.i18nc("@action:inmenu", "Manage Setting Visibility...")
iconName: "configure"
onTriggered: Cura.Actions.configureSettingVisibility.trigger()
}
ExclusiveGroup { id: group }
}

View file

@ -26,8 +26,8 @@ UM.PreferencesPage
UM.Preferences.resetPreference("general/visible_settings")
// After calling this function update Setting visibility preset combobox.
// Reset should set "Basic" setting preset
visibilityPreset.setBasicPreset()
// Reset should set default setting preset ("Basic")
visibilityPreset.setDefaultPreset()
}
resetEnabled: true;
@ -37,6 +37,8 @@ UM.PreferencesPage
id: base;
anchors.fill: parent;
property bool inhibitSwitchToCustom: false
CheckBox
{
id: toggleVisibleSettings
@ -84,7 +86,7 @@ UM.PreferencesPage
if (visibilityPreset.currentIndex != visibilityPreset.model.count - 1)
{
visibilityPreset.currentIndex = visibilityPreset.model.count - 1
UM.Preferences.setValue("general/preset_setting_visibility_choice", visibilityPreset.model.get(visibilityPreset.currentIndex).text)
UM.Preferences.setValue("cura/active_setting_visibility_preset", visibilityPreset.model.getItem(visibilityPreset.currentIndex).id)
}
}
}
@ -110,25 +112,13 @@ UM.PreferencesPage
ComboBox
{
property int customOptionValue: 100
function setBasicPreset()
function setDefaultPreset()
{
var index = 0
for(var i = 0; i < presetNamesList.count; ++i)
{
if(model.get(i).text == "Basic")
{
index = i;
break;
}
}
visibilityPreset.currentIndex = index
visibilityPreset.currentIndex = 0
}
id: visibilityPreset
width: 150
width: 150 * screenScaleFactor
anchors
{
top: parent.top
@ -137,56 +127,49 @@ UM.PreferencesPage
model: ListModel
{
id: presetNamesList
id: visibilityPresetsModel
Component.onCompleted:
{
// returned value is Dictionary (Ex: {1:"Basic"}, The number 1 is the weight and sort by weight)
var itemsDict = UM.Preferences.getValue("general/visible_settings_preset")
var sorted = [];
for(var key in itemsDict) {
sorted[sorted.length] = key;
}
visibilityPresetsModel.append({text: catalog.i18nc("@action:inmenu", "Custom selection"), id: "custom"});
sorted.sort();
for(var i = 0; i < sorted.length; i++) {
presetNamesList.append({text: itemsDict[sorted[i]], value: i});
var presets = Cura.SettingVisibilityPresetsModel;
for(var i = 0; i < presets.rowCount(); i++)
{
visibilityPresetsModel.append({text: presets.getItem(i)["name"], id: presets.getItem(i)["id"]});
}
// By agreement lets "Custom" option will have value 100
presetNamesList.append({text: "Custom", value: visibilityPreset.customOptionValue});
}
}
currentIndex:
{
// Load previously selected preset.
var text = UM.Preferences.getValue("general/preset_setting_visibility_choice");
var index = 0;
for(var i = 0; i < presetNamesList.count; ++i)
var index = Cura.SettingVisibilityPresetsModel.find("id", Cura.SettingVisibilityPresetsModel.activePreset);
if(index == -1)
{
if(model.get(i).text == text)
{
index = i;
break;
}
return 0;
}
return index;
return index + 1; // "Custom selection" entry is added in front, so index is off by 1
}
onActivated:
{
// TODO What to do if user is selected "Custom from Combobox" ?
if (model.get(index).text == "Custom"){
UM.Preferences.setValue("general/preset_setting_visibility_choice", model.get(index).text)
return
}
base.inhibitSwitchToCustom = true;
var preset_id = visibilityPresetsModel.get(index).id;
Cura.SettingVisibilityPresetsModel.setActivePreset(preset_id);
var newVisibleSettings = CuraApplication.getVisibilitySettingPreset(model.get(index).text)
UM.Preferences.setValue("general/visible_settings", newVisibleSettings)
UM.Preferences.setValue("general/preset_setting_visibility_choice", model.get(index).text)
UM.Preferences.setValue("cura/active_setting_visibility_preset", preset_id);
if (preset_id != "custom")
{
UM.Preferences.setValue("general/visible_settings", Cura.SettingVisibilityPresetsModel.getItem(index - 1).settings.join(";"));
// "Custom selection" entry is added in front, so index is off by 1
}
else
{
// Restore custom set from preference
UM.Preferences.setValue("general/visible_settings", UM.Preferences.getValue("cura/custom_visible_settings"));
}
base.inhibitSwitchToCustom = false;
}
}
@ -216,7 +199,16 @@ UM.PreferencesPage
exclude: ["machine_settings", "command_line_settings"]
showAncestors: true
expanded: ["*"]
visibilityHandler: UM.SettingPreferenceVisibilityHandler { }
visibilityHandler: UM.SettingPreferenceVisibilityHandler
{
onVisibilityChanged:
{
if(Cura.SettingVisibilityPresetsModel.activePreset != "" && !base.inhibitSwitchToCustom)
{
Cura.SettingVisibilityPresetsModel.setActivePreset("custom");
}
}
}
}
delegate: Loader
@ -259,19 +251,7 @@ UM.PreferencesPage
{
id: settingVisibilityItem;
UM.SettingVisibilityItem {
// after changing any visibility of settings, set the preset to the "Custom" option
visibilityChangeCallback : function()
{
// If already "Custom" then don't do nothing
if (visibilityPreset.currentIndex != visibilityPreset.model.count - 1)
{
visibilityPreset.currentIndex = visibilityPreset.model.count - 1
UM.Preferences.setValue("general/preset_setting_visibility_choice", visibilityPreset.model.get(visibilityPreset.currentIndex).text)
}
}
}
UM.SettingVisibilityItem { }
}
}
}

View file

@ -15,10 +15,11 @@ Item
{
id: base;
property Action configureSettings;
property bool findingSettings;
signal showTooltip(Item item, point location, string text);
signal hideTooltip();
property Action configureSettings
property bool findingSettings
property bool showingAllSettings
signal showTooltip(Item item, point location, string text)
signal hideTooltip()
Item
{
@ -107,6 +108,57 @@ Item
}
}
ToolButton
{
id: settingVisibilityMenu
width: height
height: UM.Theme.getSize("setting_control").height
anchors
{
top: globalProfileRow.bottom
topMargin: UM.Theme.getSize("sidebar_margin").height
right: parent.right
rightMargin: UM.Theme.getSize("sidebar_margin").width
}
style: ButtonStyle
{
background: Item {
UM.RecolorImage {
anchors.verticalCenter: parent.verticalCenter
anchors.horizontalCenter: parent.horizontalCenter
width: UM.Theme.getSize("standard_arrow").width
height: UM.Theme.getSize("standard_arrow").height
sourceSize.width: width
sourceSize.height: width
color: control.enabled ? UM.Theme.getColor("setting_category_text") : UM.Theme.getColor("setting_category_disabled_text")
source: UM.Theme.getIcon("menu")
}
}
label: Label{}
}
menu: SettingVisibilityPresetsMenu
{
showingSearchResults: findingSettings
showingAllSettings: showingAllSettings
onShowAllSettings:
{
base.showingAllSettings = true;
base.findingSettings = false;
filter.text = "";
filter.updateDefinitionModel();
}
onShowSettingVisibilityProfile:
{
base.showingAllSettings = false;
base.findingSettings = false;
filter.text = "";
filter.updateDefinitionModel();
}
}
}
Rectangle
{
id: filterContainer
@ -132,9 +184,9 @@ Item
top: globalProfileRow.bottom
topMargin: UM.Theme.getSize("sidebar_margin").height
left: parent.left
leftMargin: Math.round(UM.Theme.getSize("sidebar_margin").width)
right: parent.right
rightMargin: Math.round(UM.Theme.getSize("sidebar_margin").width)
leftMargin: UM.Theme.getSize("sidebar_margin").width
right: settingVisibilityMenu.left
rightMargin: Math.floor(UM.Theme.getSize("default_margin").width / 2)
}
height: visible ? UM.Theme.getSize("setting_control").height : 0
Behavior on height { NumberAnimation { duration: 100 } }
@ -168,17 +220,9 @@ Item
{
if(findingSettings)
{
expandedCategories = definitionsModel.expanded.slice();
definitionsModel.expanded = ["*"];
definitionsModel.showAncestors = true;
definitionsModel.showAll = true;
}
else
{
definitionsModel.expanded = expandedCategories;
definitionsModel.showAncestors = false;
definitionsModel.showAll = false;
showingAllSettings = false;
}
updateDefinitionModel();
lastFindingSettings = findingSettings;
}
}
@ -187,6 +231,27 @@ Item
{
filter.text = "";
}
function updateDefinitionModel()
{
if(findingSettings || showingAllSettings)
{
expandedCategories = definitionsModel.expanded.slice();
definitionsModel.expanded = [""]; // keep categories closed while to prevent render while making settings visible one by one
definitionsModel.showAncestors = true;
definitionsModel.showAll = true;
definitionsModel.expanded = ["*"];
}
else
{
if(expandedCategories)
{
definitionsModel.expanded = expandedCategories;
}
definitionsModel.showAncestors = false;
definitionsModel.showAll = false;
}
}
}
MouseArea
@ -209,7 +274,7 @@ Item
anchors.verticalCenter: parent.verticalCenter
anchors.right: parent.right
anchors.rightMargin: Math.round(UM.Theme.getSize("sidebar_margin").width)
anchors.rightMargin: UM.Theme.getSize("default_margin").width
color: UM.Theme.getColor("setting_control_button")
hoverColor: UM.Theme.getColor("setting_control_button_hover")
@ -491,9 +556,17 @@ Item
MenuItem
{
//: Settings context menu action
visible: !findingSettings;
visible: !(findingSettings || showingAllSettings);
text: catalog.i18nc("@action:menu", "Hide this setting");
onTriggered: definitionsModel.hide(contextMenu.key);
onTriggered:
{
definitionsModel.hide(contextMenu.key);
// visible settings have changed, so we're no longer showing a preset
if (Cura.SettingVisibilityPresetsModel.activePreset != "" && !showingAllSettings)
{
Cura.SettingVisibilityPresetsModel.setActivePreset("custom");
}
}
}
MenuItem
{
@ -509,7 +582,7 @@ Item
return catalog.i18nc("@action:menu", "Keep this setting visible");
}
}
visible: findingSettings;
visible: (findingSettings || showingAllSettings);
onTriggered:
{
if (contextMenu.settingVisible)
@ -520,6 +593,11 @@ Item
{
definitionsModel.show(contextMenu.key);
}
// visible settings have changed, so we're no longer showing a preset
if (Cura.SettingVisibilityPresetsModel.activePreset != "" && !showingAllSettings)
{
Cura.SettingVisibilityPresetsModel.setActivePreset("custom");
}
}
}
MenuItem

View file

@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 30 30">
<path d="m 30,23.75 v 2.5 q 0,0.50781 -0.37109,0.87891 Q 29.25781,27.5 28.75,27.5 H 1.25 Q 0.74219,27.5 0.37109,27.12891 0,26.75781 0,26.25 v -2.5 Q 0,23.24219 0.37109,22.87109 0.74219,22.5 1.25,22.5 h 27.5 q 0.50781,0 0.87891,0.37109 Q 30,23.24219 30,23.75 Z m 0,-10 v 2.5 q 0,0.50781 -0.37109,0.87891 Q 29.25781,17.5 28.75,17.5 H 1.25 Q 0.74219,17.5 0.37109,17.12891 0,16.75781 0,16.25 v -2.5 Q 0,13.24219 0.37109,12.87109 0.74219,12.5 1.25,12.5 h 27.5 q 0.50781,0 0.87891,0.37109 Q 30,13.24219 30,13.75 Z m 0,-10 v 2.5 Q 30,6.75781 29.62891,7.12891 29.25781,7.5 28.75,7.5 H 1.25 Q 0.74219,7.5 0.37109,7.12891 0,6.75781 0,6.25 V 3.75 Q 0,3.24219 0.37109,2.87109 0.74219,2.5 1.25,2.5 h 27.5 q 0.50781,0 0.87891,0.37109 Q 30,3.24219 30,3.75 Z" />
</svg>

After

Width:  |  Height:  |  Size: 817 B