diff --git a/lightdm_gtk_greeter_settings/Config.py b/lightdm_gtk_greeter_settings/Config.py new file mode 100644 index 0000000..526dcf8 --- /dev/null +++ b/lightdm_gtk_greeter_settings/Config.py @@ -0,0 +1,183 @@ +#!/usr/bin/env python3 +# -*- Mode: Python; coding: utf-8; indent-tabs-mode: nil; tab-width: 4 -*- +# LightDM GTK Greeter Settings +# Copyright (C) 2015 Andrew P. +# +# This program is free software: you can redistribute it and/or modify it +# under the terms of the GNU General Public License version 3, as published +# by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranties of +# MERCHANTABILITY, SATISFACTORY QUALITY, 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, see . + +import configparser +import os +from collections import OrderedDict +from glob import iglob + + +class Config: + + class ConfigGroup: + + def __init__(self, config): + self._config = config + self._items = OrderedDict() + + def __iter__(self): + return iter(self._items) + + def __contains__(self, item): + return item in self._items + + def __getitem__(self, item): + values = self._items.get(item) + return values[-1][1] if values else None + + def __setitem__(self, item, value): + if isinstance(value, tuple): + value, default = value + else: + default = None + + values = self._items.get(item) + if values and values[-1][0] == self._config._output_path: + if default is not None and value == default and len(values) == 1: + values.clear() + else: + values[-1] = (self._config._output_path, value) + elif values is not None: + if default is None or value != default or (values and values[-1][1] != default): + values.append((self._config._output_path, value)) + else: + if default is None or value != default: + self._items[item] = [(self._config._output_path, value)] + + def __delitem__(self, item): + values = self._items.get(item) + if values is not None: + if values and values[-1][0] == self._config._output_path: + del values[-1] + if not values: + del self._items[item] + + def get_key_file(self, key): + values = self._items.get(key) + return values[-1][0] if values else None + + def __init__(self, input_pathes, output_path): + self._input_pathes = tuple(input_pathes) + self._output_path = output_path + self._groups = OrderedDict() + + def read(self): + files = [] + for path in self._input_pathes: + if os.path.isdir(path): + files.extend(sorted(iglob(os.path.join(path, '*.conf')))) + elif os.path.exists(path): + files.append(path) + if self._output_path not in files: + files.append(self._output_path) + + self._groups.clear() + for path in files: + config_file = configparser.RawConfigParser(strict=False, allow_no_value=True) + config_file.read(path) + + for groupname, values in config_file.items(): + if groupname == 'DEFAULT': + continue + + if groupname not in self._groups: + self._groups[groupname] = Config.ConfigGroup(self) + group = self._groups[groupname] + + for key, value in values.items(): + if key in group._items: + values = group._items[key] + if value is not None or values: + values.append((path, value)) + elif value is not None: + group._items[key] = [(path, value)] + + def write(self): + config_file = configparser.RawConfigParser(strict=False, allow_no_value=True) + + for groupname, group in self._groups.items(): + config_file.add_section(groupname) + config_section = config_file[groupname] + + for key, values in group._items.items(): + if not values or values[-1][0] != self._output_path: + continue + if values[-1][1] is not None or len(values) > 1: + config_section[key] = values[-1][1] + + with open(self._output_path, 'w') as file: + config_file.write(file) + + def items(self): + return self._groups.items() + + def allitems(self): + return ((g, k, items[k]) for (g, items) in self._groups.items() for k in items._items) + + def add_group(self, name): + if name in self._groups: + return self._groups[name] + else: + return self._groups.setdefault(name, Config.ConfigGroup(self)) + + def get_key_file(self, groupname, key): + group = self._groups.get(groupname) + return group.get_key_file(key) if group is not None else None + + def __iter__(self): + return iter(self._groups) + + def __getitem__(self, item): + if isinstance(item, tuple): + group = self._groups.get(item[0]) + return group[item[1]] if group else None + return self._groups.get(item) + + def __setitem__(self, item, value): + if isinstance(item, tuple): + if not item[0] in self._groups: + self._groups = Config.ConfigGroup(self) + self._groups[item[0]][item[1]] = value + + def __delitem__(self, item): + if isinstance(item, tuple): + group = self._groups.get(item[0]) + if group is not None: + del group[item[1]] + return + + group = self._groups.get(item) + if group is not None: + if not group: + del self._groups[item] + return + + keys_to_remove = [] + for key, values in group._items.items(): + if values[-1][0] == self._output_path: + if len(values) == 1: + keys_to_remove.append(key) + else: + values[-1] = (self._output_path, None) + elif values: + values.append((self._output_path, None)) + + if len(keys_to_remove) < len(group._items): + for key in keys_to_remove: + del group._items[key] + else: + del self._groups[item] diff --git a/lightdm_gtk_greeter_settings/GtkGreeterSettingsWindow.py b/lightdm_gtk_greeter_settings/GtkGreeterSettingsWindow.py index 8bbdcb0..a503c42 100644 --- a/lightdm_gtk_greeter_settings/GtkGreeterSettingsWindow.py +++ b/lightdm_gtk_greeter_settings/GtkGreeterSettingsWindow.py @@ -17,22 +17,23 @@ import collections -import configparser import os import shlex import sys +from functools import partialmethod from glob import iglob -from functools import partialmethod from itertools import chain from locale import gettext as _ from gi.repository import ( Gdk, + GLib, Gtk) from gi.repository import Pango from gi.repository.GObject import markup_escape_text as escape_markup from lightdm_gtk_greeter_settings import ( + Config, helpers, IconEntry, IndicatorsEntry, @@ -138,8 +139,17 @@ group.entry_added.connect(self.on_entry_added) group.entry_removed.connect(self.on_entry_removed) - self._config_path = helpers.get_config_path() - self._allow_edit = self._has_access_to_write(self._config_path) + config_pathes = [] + config_pathes.extend(os.path.join(p, 'lightdm-gtk-greeter.conf.d') + for p in GLib.get_system_data_dirs()) + config_pathes.extend(os.path.join(p, 'lightdm-gtk-greeter.conf.d') + for p in GLib.get_system_config_dirs()) + config_pathes.append(os.path.join(os.path.dirname(helpers.get_config_path()), + 'lightdm-gtk-greeter.conf.d')) + + self._config = Config.Config(config_pathes, helpers.get_config_path()) + + self._allow_edit = self._has_access_to_write(helpers.get_config_path()) self._widgets.apply.props.visible = self._allow_edit if not self._allow_edit: @@ -151,7 +161,7 @@ secondary_text=_( 'It seems that you don\'t have permissions to write to ' 'file:\n{path}\n\nTry to run this program using "sudo" ' - 'or "pkexec"').format(path=self._config_path), + 'or "pkexec"').format(path=helpers.get_config_path()), message_type=Gtk.MessageType.WARNING) if self.mode == WindowMode.Embedded: @@ -176,13 +186,12 @@ self.set_titlebar(header) - self._config = configparser.RawConfigParser(strict=False) self._read() def _has_access_to_write(self, path): - if os.path.exists(path) and os.access(self._config_path, os.W_OK): + if os.path.exists(path) and os.access(helpers.get_config_path(), os.W_OK): return True - return os.access(os.path.dirname(self._config_path), os.W_OK | os.X_OK) + return os.access(os.path.dirname(helpers.get_config_path()), os.W_OK | os.X_OK) def _set_message(self, message, type_=Gtk.MessageType.INFO): if not message: @@ -193,17 +202,7 @@ self._widgets.infobar.show() def _read(self): - self._config.clear() - try: - if not self._config.read(self._config_path) and \ - self.mode != WindowMode.Embedded: - helpers.show_message(text=_('Failed to read configuration file: {path}') - .format(path=self._config_path), - message_type=Gtk.MessageType.ERROR) - except (configparser.DuplicateSectionError, - configparser.MissingSectionHeaderError): - pass - + self._config.read() self._changed_entries = None for group in self._groups: @@ -226,8 +225,7 @@ self._widgets.apply.props.sensitive = False try: - with open(self._config_path, 'w') as file: - self._config.write(file) + self._config.write() except OSError as e: helpers.show_message(e, Gtk.MessageType.ERROR) @@ -321,6 +319,7 @@ class EntryMenu: menu = Gtk.Menu() value = new_item() + file = new_item() error_separator = Gtk.SeparatorMenuItem() error = new_item() error_action = new_item(self.on_entry_fix_clicked) @@ -329,6 +328,7 @@ default = new_item(self.on_entry_reset_clicked) menu.append(value) + menu.append(file) menu.append(error_separator) menu.append(error) menu.append(error_action) @@ -355,6 +355,17 @@ group=group.name, key=key, value=format_value(value=entry.value, enabled=entry.enabled)) + + key_file = None + if entry not in self._changed_entries: + key_file = self._config.get_key_file(group.name, key) + if key_file and key_file == helpers.get_config_path(): + key_file = None + elif key_file: + menu.file.props.label = _('Value defined in file: {path}')\ + .format(path=escape_markup(key_file)) + menu.file.set_tooltip_text(key_file) + menu.file.props.visible = key_file is not None error = entry.error error_action = None @@ -367,7 +378,7 @@ menu.error_action.props.label = label or '' if error_action: menu.error_action._fix_entry_data = entry, error_action - menu.error.set_label(error) + menu.error.set_label(escape_markup(error)) menu.error.props.visible = error is not None menu.error_action.props.visible = error_action is not None diff --git a/lightdm_gtk_greeter_settings/MonitorsGroup.py b/lightdm_gtk_greeter_settings/MonitorsGroup.py index 56f0138..2d9a7f6 100644 --- a/lightdm_gtk_greeter_settings/MonitorsGroup.py +++ b/lightdm_gtk_greeter_settings/MonitorsGroup.py @@ -44,28 +44,31 @@ def read(self, config): self._entries.clear() - for name, section in config.items(): + for name, group in config.items(): if not name.startswith(self.GROUP_PREFIX): continue name = name[len(self.GROUP_PREFIX):].strip() entry = MonitorEntry(self._widgets) - entry['background'] = section.get('background', None) - entry['user-background'] = bool2string(section.getboolean('user-background', None), 1) - entry['laptop'] = bool2string(section.getboolean('laptop', None), True) + entry['background'] = group['background'] + entry['user-background'] = bool2string(group['user-background'], True) + entry['laptop'] = bool2string(group['laptop'], True) self._entries[name] = entry self.entry_added.emit(entry, name) def write(self, config): - for name in config.sections(): - if name.startswith(self.GROUP_PREFIX): - config.remove_section(name) + groups = set(name for name, __ in self._entries.items()) + groups_to_remove = tuple(name for name in config + if (name.startswith(self.GROUP_PREFIX) and + name[len(self.GROUP_PREFIX):].strip() not in groups)) for name, entry in self._entries.items(): - section = '{prefix} {name}'.format(prefix=self.GROUP_PREFIX, name=name.strip()) - config.add_section(section) + groupname = '{prefix} {name}'.format(prefix=self.GROUP_PREFIX, name=name.strip()) + group = config.add_group(groupname) for key, value in entry: - if value is not None: - config.set(section, key, value) + group[key] = value + + for name in groups_to_remove: + del config[name] def _on_label_link_activate(self, label, uri): if not self._dialog: diff --git a/lightdm_gtk_greeter_settings/OptionGroup.py b/lightdm_gtk_greeter_settings/OptionGroup.py index ded6132..8a3b995 100644 --- a/lightdm_gtk_greeter_settings/OptionGroup.py +++ b/lightdm_gtk_greeter_settings/OptionGroup.py @@ -44,27 +44,31 @@ self.__defaults_wrapper = None def read(self, config): + '''Read group content from specified GreeterConfig object''' raise NotImplementedError(self.__class__) def write(self, config): + '''Writes content of this group to specified GreeterConfig object''' raise NotImplementedError(self.__class__) @property def entries(self): + '''entries["key"] - key => Entry mapping. Read only.''' if not self.__entries_wrapper: self.__entries_wrapper = BaseGroup.__DictWrapper(self._get_entry) return self.__entries_wrapper @property def defaults(self): + '''defaults["key"] - default value for "key" entry. Read only.''' if not self.__defaults_wrapper: self.__defaults_wrapper = BaseGroup.__DictWrapper(self._get_default) return self.__defaults_wrapper - def _get_default(self, key): + def _get_entry(self, key): raise NotImplementedError(self.__class__) - def _get_entry(self, key): + def _get_default(self, key): raise NotImplementedError(self.__class__) @GObject.Signal @@ -93,7 +97,6 @@ return self._name def read(self, config): - if not self._entries: for key, (klass, default) in self._options.items(): entry = klass(WidgetsWrapper(self._widgets, key)) @@ -102,24 +105,15 @@ self.entry_added.emit(entry, key) for key, entry in self._entries.items(): - if config.has_option(self._name, key): - entry.value = config.get(self._name, key) - entry.enabled = True - else: - entry.value = self._defaults[key] - entry.enabled = False + value = config[self._name, key] + entry.value = value if value is not None else self._defaults[key] + entry.enabled = value is not None def write(self, config): - - if not config.has_section(self._name): - config.add_section(self._name) - for key, entry in self._entries.items(): - value = entry.value - if entry.enabled and value != self._get_default(key): - config.set(self._name, key, entry.value) - else: - config.remove_option(self._name, key) + del config[self._name, key] + if entry.enabled: + config[self._name, key] = entry.value, self._get_default(key) def _get_entry(self, key): return self._entries.get(key) diff --git a/lightdm_gtk_greeter_settings/helpers.py b/lightdm_gtk_greeter_settings/helpers.py index 49c75d7..0700077 100644 --- a/lightdm_gtk_greeter_settings/helpers.py +++ b/lightdm_gtk_greeter_settings/helpers.py @@ -118,6 +118,8 @@ def bool2string(value, skip_none=False): + if isinstance(value, str): + value = string2bool(value) return 'true' if value else 'false' if not skip_none or value is not None else None