File: //bin/xfce4-screensaver-configure
#!/usr/bin/python3
# -*- Mode: Python; coding: utf-8; indent-tabs-mode: nil; tab-width: 4 -*-
#
# Copyright (C) 2019 Sean Davis <bluesabre@xfce.org>
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA.
#
# Authors: Sean Davis <bluesabre@xfce.org>
import argparse
import os
import sys
import xml.etree.ElementTree as ET
import warnings
from collections import OrderedDict
from locale import gettext as _
import gi
gi.require_version('GLib', '2.0')
gi.require_version('GObject', '2.0')
gi.require_version('Gdk', '3.0')
gi.require_version('Gtk', '3.0')
gi.require_version('Libxfce4util', '1.0')
gi.require_version('Xfconf', '0')
from gi.repository import GLib, GObject, Gdk, Gtk, Libxfce4util, Xfconf
warnings.filterwarnings("ignore")
class XfconfSettings(object):
def __init__(self, channel_obj, prop_base):
self.channel = channel_obj
self.prefix = prop_base
def _check_property_name(self, name):
if name.startswith('/'):
return name
else:
return '{0}/{1}'.format(self.prefix, name)
def restore_defaults(self):
self.channel.reset_property(self.prefix, True)
def get_boolean(self, prop):
g_value = GObject.Value(GObject.TYPE_BOOLEAN)
self.channel.get_property(self._check_property_name(prop),
g_value)
if g_value.get_boolean() in ["true", True]:
return True
return False
def set_boolean(self, prop, value):
g_value = GObject.Value(GObject.TYPE_BOOLEAN)
g_value.set_boolean(value)
self.channel.set_property(self._check_property_name(prop),
g_value)
def get_double(self, prop):
g_value = GObject.Value(GObject.TYPE_DOUBLE)
self.channel.get_property(self._check_property_name(prop),
g_value)
return g_value.get_double()
def set_double(self, prop, value):
g_value = GObject.Value(GObject.TYPE_DOUBLE)
g_value.set_double(float(value))
self.channel.set_property(self._check_property_name(prop),
g_value)
def get_int(self, prop):
g_value = GObject.Value(GObject.TYPE_INT)
self.channel.get_property(self._check_property_name(prop),
g_value)
return g_value.get_int()
def set_int(self, prop, value):
g_value = GObject.Value(GObject.TYPE_INT)
g_value.set_int(int(value))
self.channel.set_property(self._check_property_name(prop),
g_value)
def get_string(self, prop):
g_value = GObject.Value(GObject.TYPE_STRING)
self.channel.get_property(self._check_property_name(prop),
g_value)
return g_value.get_string()
def set_string(self, prop, value):
g_value = GObject.Value(GObject.TYPE_STRING)
g_value.set_string(value)
self.channel.set_property(self._check_property_name(prop),
g_value)
def check_property(self, prop):
return self.channel.has_property(self._check_property_name(prop))
class ScreensaverSettings:
def __init__(self):
self.name = None
self.label = None
self.arguments = None
self.options = OrderedDict()
self.description = None
self.video = None
self.filename = None
self.valid = False
self.configurable = False
def to_dict(self):
parsed = {}
parsed['name'] = self.name
parsed['label'] = self.label
parsed['arguments'] = self.arguments
parsed['options'] = self.options
parsed['description'] = self.description
parsed['video'] = self.video
for key in parsed:
if parsed[key] is None:
parsed[key] = ""
return parsed
class DesktopScreensaverSettings(ScreensaverSettings):
def __init__(self, name):
ScreensaverSettings.__init__(self)
self.name = name
def load_from_file(self, filename):
if self.name == "xfce-blank":
self.valid = True
self.options = self.parse_internal()
self.label = _("Blank screen")
comment = _("Powered by Display Power Management Signaling (DPMS),\n"
"Xfce Screensaver can automatically suspend your displays\n"
"to save power.\n\n"
"Xfce Power Manager and other applications also manage\n"
"DPMS settings. If the settings here won't work, make sure\n"
"display power management wasn't disabled there.\n"
"If your displays are powering off at different\n"
"intervals, be sure to check for conflicting settings.")
self.arguments = []
self.description = comment
if self.options:
self.configurable = True
return True
if not os.path.exists(filename):
return False
self.filename = filename
keyfile = GLib.KeyFile.new()
if not keyfile.load_from_file(filename, GLib.KeyFileFlags.NONE):
return False
self.valid = True
label = keyfile.get_locale_string("Desktop Entry", "Name", None)
comment = keyfile.get_locale_string("Desktop Entry", "Comment", None)
command = keyfile.get_string("Desktop Entry", "Exec")
if self.name in ["xfce-floaters", "xfce-personal-slideshow", "xfce-popsquares"]:
options = self.parse_internal()
self.label = label
self.arguments = command.split(" ")[1:]
self.options = options
self.description = "%s\n\n%s" % (comment, _("Part of Xfce Screensaver"))
if self.options:
self.configurable = True
return True
def parse_internal(self):
options = OrderedDict()
if self.name == "xfce-blank":
options["dpms-sleep-after"] = {'id': 'dpms-sleep-after', 'type': 'slider',
'label': _("After blanking, put display to sleep after:"),
'default': 5, 'low': 0, 'high': 60, 'step': 1,
'low-label': _("Never"), 'high-label': _("60 seconds")}
options["dpms-off-after"] = {'id': 'dpms-off-after', 'type': 'slider',
'label': _("After sleeping, switch display off after:"),
'default': 15, 'low': 0, 'high': 60, 'step': 1,
'low-label': _("Never"), 'high-label': _("60 minutes")}
if self.name == "xfce-floaters":
options["number-of-images"] = {'id': 'number-of-images', 'type': 'spinbutton',
'label': _("Max number of images"), 'argument': '-n %',
'default': 5, 'low': 1, 'high': 1000}
options["show-paths"] = {'id': 'show-paths', 'type': 'checkbox',
'label': _("Show paths"), 'argument': '-p'}
options["do-rotations"] = {'id': 'do-rotations', 'type': 'checkbox',
'label': _("Do rotations"), 'argument': '-r'}
options["print-stats"] = {'id': 'print-stats', 'type': 'checkbox',
'label': _("Print stats"), 'argument': '-s'}
elif self.name == "xfce-personal-slideshow":
pics = GLib.get_user_special_dir(GLib.UserDirectory.DIRECTORY_PICTURES)
options["location"] = {'id': 'location', 'type': 'folder', 'label': _("Location"),
'argument': '--location=%', 'default': pics if pics != None else '~'}
options["background-color"] = {'id': 'background-color', 'type': 'color',
'label': _("Background color"), 'argument': '--background-color=%', 'default': '#000000'}
options["sort-images"] = {'id': 'sort-images', 'type': 'checkbox',
'label': _("Do not randomize images"), 'argument': '--sort-images'}
options["no-stretch"] = {'id': 'no-stretch', 'type': 'checkbox',
'label': _("Do not stretch images"), 'argument': '--no-stretch'}
return options
class XmlScreensaverSettings(ScreensaverSettings):
def __init__(self, name):
ScreensaverSettings.__init__(self)
self.name = name
def load_from_file(self, filename):
if not os.path.exists(filename):
return False
self.filename = filename
element = ET.parse(filename).getroot()
root = self.parse_element(element)
self.name = root["attributes"]["name"]
self.label = root["attributes"]["label"]
self.arguments = []
for item in self.get_data_elements(element):
parsed = self.parse_element(item)
element_type = parsed["type"]
parsed.pop("type", None)
if element_type == "property":
if parsed["id"] == "command":
self.arguments.append(parsed["attributes"]["argument"])
elif parsed["id"] == "description":
self.description = parsed["text"]
elif parsed["id"] == "video":
self.video = parsed["attributes"]["href"]
elif element_type == "option":
if "options" in parsed.keys():
parsed["attributes"]["options"] = parsed["options"]
self.options[parsed["id"]] = parsed["attributes"]
if self.options:
self.configurable = True
return True
def get_data_elements(self, element):
results = []
for child in list(element):
if child.tag == "vgroup" or child.tag == "hgroup":
results = results + self.get_data_elements(child)
elif child.tag.startswith("xscreensaver"):
pass
else:
if child.tag.startswith("_"):
child.tag = child.tag[1:]
if child.text is not None:
child.text = _(child.text)
results.append(child)
return results
def parse_element_attributes(self, element):
attributes = {
"id": "",
"type": "",
"label": "",
}
for key in element.attrib:
value = element.attrib[key]
if key.startswith("_"):
key = key[1:]
if value is not None:
value = _(value)
if key in ["arg", "arg-set", "arg-unset"]:
key = "argument"
if "#" in value:
tmp = value.split(" ")
parts = []
for piece in tmp:
if piece.startswith("#"):
piece = "'%s'" % piece
parts.append(piece)
value = " ".join(parts)
attributes[key] = value
return attributes
def parse_element_id(self, element, attributes):
if attributes["id"]:
element_id = attributes["id"]
element_type = "option"
else:
element_id = element.tag
element_type = "property"
return (element_id, element_type)
def parse_element(self, element):
text = None
options = None
attributes = self.parse_element_attributes(element)
element_id, element_type = self.parse_element_id(element, attributes)
if element.tag == "select":
attributes["type"] = "select"
options = []
for child in element.findall("option"):
options.append(self.parse_element(child))
elif element.tag == "option":
if not attributes["id"] and attributes["label"]:
element_id = attributes["label"].replace(" ", "-").lower()
out = {"id": element_id, "label": attributes["label"]}
if "argument" in attributes.keys():
out["argument"] = attributes["argument"]
return out
if attributes["type"] in ["slider", "spinbutton"]:
for attrib in ["high", "low", "default"]:
if attrib in attributes.keys():
attributes[attrib] = float(attributes[attrib])
if not attributes["type"]:
if "argument" in attributes.keys():
attributes["type"] = "checkbox"
if attributes["id"] == "file":
attributes["type"] = "file"
if attributes["label"] == "":
attributes["label"] = attributes["id"].capitalize()
out = {"id": element_id, "type": element_type, "attributes": attributes}
if element.text:
text = str(element.text).strip()
if text:
out["text"] = text
if options:
out["options"] = options
return out
class ConfigurationWindow(Gtk.Window):
def __init__(self, parsed):
Gtk.Window.__init__(self, title=parsed["label"])
self.set_modal(True)
self.set_border_width(6)
self.set_default_size(400, -1)
self.set_icon_name("preferences-desktop-screensaver")
self.inner_margin = 12
self.screensaver_args = parsed["arguments"]
property_base = '/screensavers/{0}'.format(parsed['name'])
channel = Xfconf.Channel.get('xfce4-screensaver')
if channel is not None:
self.xfconf_settings = XfconfSettings(channel,
property_base)
else:
print("Unable to access channel", file=sys.stderr)
sys.exit(1)
self.notebook = Gtk.Notebook.new()
self.grid = Gtk.Grid.new()
self.widgets = dict()
self.lookup_table = OrderedDict()
self.defaults = {}
self.settings = OrderedDict()
self.add(self.notebook)
self.setup_grid()
self.setup_notebook(parsed)
row = 0
for opt_key in parsed["options"]:
opt = parsed["options"][opt_key]
label = Gtk.Label.new(opt["label"])
label.set_halign(Gtk.Align.START)
self.grid.attach(label, 0, row, 1, 1)
widget = self.get_option_widget(opt)
widget.set_hexpand(True)
self.grid.attach(widget, 1, row, 1, 1)
self.widgets[opt_key] = (widget, opt["type"])
row += 1
widget = Gtk.Button.new_with_label(_("Restore Defaults"))
widget.set_margin_top(18)
widget.connect("clicked", self.on_restore_clicked)
self.grid.attach(widget, 0, row, 2, 1)
def setup_grid(self):
self.grid.set_column_spacing(12)
self.grid.set_row_spacing(6)
self.grid.set_margin_top(self.inner_margin)
self.grid.set_margin_bottom(self.inner_margin)
self.grid.set_margin_start(self.inner_margin)
self.grid.set_margin_end(self.inner_margin)
label = Gtk.Label.new(_("Preferences"))
self.notebook.append_page(self.grid, label)
def setup_notebook(self, parsed):
if parsed["description"] != "":
vbox = Gtk.Box.new(Gtk.Orientation.VERTICAL, 18)
label = Gtk.Label.new(_("About"))
self.notebook.append_page(vbox, label)
vbox.set_margin_top(self.inner_margin)
vbox.set_margin_bottom(self.inner_margin)
vbox.set_margin_start(self.inner_margin)
vbox.set_margin_end(self.inner_margin)
label = Gtk.Label.new(parsed["description"])
label.set_valign(Gtk.Align.START)
vbox.pack_start(label, True, True, 0)
if parsed["video"] != "":
button = Gtk.LinkButton.new_with_label(
parsed["video"], _("Video"))
vbox.pack_start(button, False, False, 0)
else:
self.notebook.set_show_tabs(False)
def get_option_widget(self, opt):
if "argument" in opt.keys():
self.lookup_table[opt["id"]] = opt["argument"]
if opt["type"] == "checkbox":
widget = Gtk.CheckButton.new_with_label("")
if self.xfconf_settings.check_property(opt['id']):
active = self.xfconf_settings.get_boolean(opt['id'])
self.defaults[opt['id']] = False
else:
active = self.get_boolean(opt['id'], False)
widget.set_active(active)
widget.connect("toggled", self.on_checkbutton_toggle, opt["id"])
elif opt["type"] == "file":
widget = Gtk.FileChooserButton(
title=opt["label"], action=Gtk.FileChooserAction.OPEN)
filename = self.get_string(opt["id"], "")
widget.set_current_folder("~")
widget.set_filename(filename)
widget.connect("selection-changed",
self.on_file_changed, opt["id"])
elif opt["type"] == "folder":
widget = Gtk.FileChooserButton(
title=opt["label"], action=Gtk.FileChooserAction.SELECT_FOLDER)
folder = self.get_string(opt["id"], opt["default"])
widget.set_current_folder(folder)
widget.set_filename(folder)
widget.connect("selection-changed",
self.on_file_changed, opt["id"])
elif opt["type"] == "select":
self.lookup_table[opt["id"]] = {}
widget = Gtk.ComboBoxText.new()
for option in opt["options"]:
opt_id = get_key(option, "id", "")
opt_label = get_key(option, "label", "")
opt_argument = get_key(option, "argument", "")
widget.append(opt_id, opt_label)
self.lookup_table[opt["id"]][opt_id] = opt_argument
if self.xfconf_settings.check_property(opt['id']):
current = self.xfconf_settings.get_string(opt['id'])
self.defaults[opt['id']] = ''
else:
current = self.get_string(opt['id'], '')
widget.set_active_id(current)
widget.connect("changed", self.on_select_changed, opt["id"])
elif opt["type"] == "slider":
prefs = get_slider_prefs([opt["default"], opt["low"], opt["high"]])
adj = Gtk.Adjustment(value=opt["default"], lower=opt["low"], upper=opt["high"],
step_increment=prefs['step'], page_increment=10, page_size=0)
widget = Gtk.Scale(
orientation=Gtk.Orientation.HORIZONTAL, adjustment=adj)
widget.add_mark(
opt["low"], Gtk.PositionType.BOTTOM, opt["low-label"])
widget.add_mark(
opt["high"], Gtk.PositionType.BOTTOM, opt["high-label"])
widget.set_digits(prefs['digits'])
if self.xfconf_settings.check_property(opt['id']):
value = self.xfconf_settings.get_double(opt['id'])
self.defaults[opt['id']] = opt['default']
else:
value = self.get_double(opt['id'], opt['default'])
adj.set_value(value)
widget.connect("value-changed", self.on_double_changed, opt["id"])
elif opt["type"] == "spinbutton":
widget = Gtk.SpinButton.new_with_range(opt["low"], opt["high"], 1)
if self.xfconf_settings.check_property(opt['id']):
value = self.xfconf_settings.get_int(opt['id'])
self.defaults[opt['id']] = opt['default']
else:
value = self.get_int(opt['id'], opt['default'])
widget.set_value(value)
widget.connect("value-changed", self.on_int_changed, opt["id"])
elif opt["type"] == "color":
widget = Gtk.ColorButton.new()
color = self.get_string(opt["id"], "#000000")
widget.set_rgba(hex_to_rgba(color))
widget.connect("color-set", self.on_color_changed, opt["id"])
else:
print("Unrecognized type: %s" % opt["type"], file=sys.stderr)
sys.exit(1)
return widget
def get_boolean(self, option_id, default):
if self.xfconf_settings.check_property(option_id):
value = self.xfconf_settings.get_boolean(option_id)
else:
value = default
self.defaults[option_id] = default
self.set_boolean(option_id, value, False)
return value
def set_boolean(self, option_id, value, store=True):
self.settings[option_id] = value
if store:
self.xfconf_settings.set_boolean(option_id, value)
def get_double(self, option_id, default):
if self.xfconf_settings.check_property(option_id):
value = self.xfconf_settings.get_double(option_id)
else:
value = default
self.defaults[option_id] = default
self.set_double(option_id, value, False)
return value
def set_double(self, option_id, value, store=True):
self.settings[option_id] = value
if store:
self.xfconf_settings.set_double(option_id, value)
def get_int(self, option_id, default):
if self.xfconf_settings.check_property(option_id):
value = self.xfconf_settings.get_int(option_id)
else:
value = default
self.defaults[option_id] = default
self.set_int(option_id, value, False)
return value
def set_int(self, option_id, value, store=True):
self.settings[option_id] = value
if store:
self.xfconf_settings.set_int(option_id, value)
def get_string(self, option_id, default):
if self.xfconf_settings.check_property(option_id):
value = self.xfconf_settings.get_string(option_id)
else:
value = default
self.defaults[option_id] = default
self.set_string(option_id, value, False)
return value
def set_string(self, option_id, value, store=True):
self.settings[option_id] = value
if store:
self.xfconf_settings.set_string(option_id, value)
def on_checkbutton_toggle(self, button, option_id):
self.set_boolean(option_id, button.get_active())
self.write_arguments()
def on_file_changed(self, button, option_id):
filename = button.get_filename()
self.set_string(option_id, filename)
self.write_arguments()
def on_select_changed(self, combobox, option_id):
active = combobox.get_active_id()
self.set_string(option_id, active)
self.write_arguments()
def on_double_changed(self, slider, option_id):
value = slider.get_value()
self.set_double(option_id, value)
self.write_arguments()
def on_int_changed(self, slider, option_id):
value = int(slider.get_value())
self.set_int(option_id, value)
self.write_arguments()
def on_color_changed(self, widget, option_id):
rgba = widget.get_rgba()
self.set_string(option_id, rgba_to_hex(rgba))
self.write_arguments()
def on_restore_clicked(self, button):
for widget_id in self.widgets:
widget, widget_type = self.widgets[widget_id]
if widget_type == "checkbox":
widget.set_active(self.defaults[widget_id])
elif widget_type == "file":
widget.set_filename(self.defaults[widget_id])
elif widget_type == "folder":
widget.set_filename(self.defaults[widget_id])
widget.set_current_folder(self.defaults[widget_id])
elif widget_type == "select":
widget.set_active(0)
elif widget_type == "slider":
widget.set_value(self.defaults[widget_id])
elif widget_type == "spinbutton":
widget.set_value(self.defaults[widget_id])
elif widget_type == "color":
color = self.defaults[widget_id]
widget.set_rgba(hex_to_rgba(color))
self.set_string(widget_id, color)
self.xfconf_settings.restore_defaults()
def write_arguments(self):
arguments = []
for setting in self.settings:
value = self.settings[setting]
if value not in ("", False):
if setting in self.lookup_table:
argument = self.lookup_table[setting]
if isinstance(argument, dict):
argument = argument[value]
elif "%" in argument:
try:
value = int(value)
except:
pass
argument = argument.replace("%", str(value))
arguments.append(argument)
value = " ".join(arguments)
self.xfconf_settings.set_string("arguments", value)
def get_slider_prefs(options):
digits = 0
for val in options:
parts = str(val).split(".")
if len(parts) < 2:
digits = 0
else:
val = parts[-1]
if val != "0":
val = val.rstrip("0")
digits = max(digits, len(val))
if digits == 0:
return {'digits': 0, 'step': 1}
return {'digits': digits, 'step': 10 ^ (-1 * digits)}
def rgba_to_hex(rgba):
value = '#%02x%02x%02x' % (int(round(
rgba.red * 255, 0)), int(round(rgba.green * 255, 0)), int(round(rgba.blue * 255, 0)))
value = value.upper()
return value
def hex_to_rgba(value):
rgba = Gdk.RGBA()
rgba.parse(value)
return rgba
def get_filename(theme):
user_data = [GLib.get_user_data_dir()]
system_data = GLib.get_system_data_dirs()
data_dirs = user_data + [i for i in system_data if i not in user_data]
for config_file in ["%s/xscreensaver/config/%s.xml", "%s/applications/screensavers/%s.desktop"]:
for data_dir in data_dirs:
cfg = config_file % (data_dir, theme)
if os.path.exists(cfg):
return cfg
if theme == "xfce-blank":
return ""
return None
def get_key(dct, keyname, default=""):
if keyname in dct.keys():
return dct[keyname]
return default
def configure(parsed):
try:
win = ConfigurationWindow(parsed)
win.connect("destroy", Gtk.main_quit)
win.set_position(Gtk.WindowPosition.MOUSE)
win.show_all()
Gtk.main()
except KeyboardInterrupt:
pass
def show_fatal(primary, secondary):
if not graphical:
print("%s: %s" % (primary, secondary), file=sys.stderr)
sys.exit(1)
try:
dlg = Gtk.MessageDialog(None, 0, Gtk.MessageType.ERROR,
Gtk.ButtonsType.OK, primary)
dlg.format_secondary_text(secondary)
dlg.set_position(Gtk.WindowPosition.MOUSE)
dlg.run()
except KeyboardInterrupt:
pass
sys.exit(1)
if __name__ == "__main__":
Libxfce4util.textdomain('xfce4-screensaver',
os.path.join(os.path.dirname(os.path.realpath(__file__)), '../share/locale'),
'UTF-8')
parser = argparse.ArgumentParser(
description=_("Configure an individual screensaver"))
parser.add_argument('screensaver', metavar='N', type=str, nargs='?',
help=_("screensaver name to configure"))
parser.add_argument('--check', action='store_true',
help=_("check if screensaver is configurable"))
args = parser.parse_args()
graphical = not args.check
primary = _("Unable to configure screensaver")
saver = args.screensaver
if saver is None:
show_fatal(primary, _("Screensaver required."))
sys.exit(1)
if saver.startswith("screensavers-"):
saver = saver[13:]
fname = get_filename(saver)
if fname is None:
show_fatal(primary, _("File not found: %s") % saver)
sys.exit(1)
if fname.endswith(".xml"):
obj = XmlScreensaverSettings(saver)
elif fname.endswith(".desktop") or saver == "xfce-blank":
obj = DesktopScreensaverSettings(saver)
else:
show_fatal(primary, _("Unrecognized file type: %s") % fname)
sys.exit(1)
if not obj.load_from_file(fname):
show_fatal(primary, _("Failed to process file: %s") % fname)
sys.exit(1)
if args.check:
if obj.configurable:
print(_("Screensaver %s is configurable.") % saver)
else:
print(_("Screensaver %s is not configurable.") % saver)
sys.exit(0)
if not obj.configurable:
show_fatal(primary, _("Screensaver %s has no configuration options.") % saver)
if not Xfconf.init():
print("Failed to initialize Xfconf", file=sys.stderr)
sys.exit(1)
configure(obj.to_dict())
Xfconf.shutdown()