Exemplo n.º 1
0
    def _edit_indicator(self, options, add_callback=None):
        if not self._properties_dialog:
            from lightdm_gtk_greeter_settings.IndicatorPropertiesDialog \
                import IndicatorPropertiesDialog as Dialog
            self._properties_dialog = Dialog(is_duplicate=self._is_duplicate,
                                             get_defaults=self.DefaultOptions.get,
                                             get_name=self.Names.get)
            self._properties_dialog.props.transient_for = self._treeview.get_toplevel()

        self._properties_dialog.add_callback = add_callback
        self._properties_dialog.set_indicator(options)
        if self._properties_dialog.run() == Gtk.ResponseType.OK:
            options = self._properties_dialog.get_indicator()
        else:
            options = None
        self._properties_dialog.hide()
        return options
Exemplo n.º 2
0
    def _edit_indicator(self, options, add_callback=None):
        if not self._properties_dialog:
            from lightdm_gtk_greeter_settings.IndicatorPropertiesDialog \
                import IndicatorPropertiesDialog as Dialog
            self._properties_dialog = Dialog(
                is_duplicate=self._is_duplicate,
                get_defaults=self.DefaultOptions.get,
                get_name=self.Names.get)
            self._properties_dialog.props.transient_for = self._treeview.get_toplevel(
            )

        self._properties_dialog.add_callback = add_callback
        self._properties_dialog.set_indicator(options)
        if self._properties_dialog.run() == Gtk.ResponseType.OK:
            options = self._properties_dialog.get_indicator()
        else:
            options = None
        self._properties_dialog.hide()
        return options
Exemplo n.º 3
0
class IndicatorsEntry(BaseEntry):
    # Readable names for indicators
    Names = {
        Indicators.External:    C_('option-entry|indicators|name', 'External library/service'),
        Indicators.Spacer:      C_('option-entry|indicators|name', 'Spacer'),
        Indicators.Separator:   C_('option-entry|indicators|name', 'Separator'),
        Indicators.Text:        C_('option-entry|indicators|name', 'Text'),
        Indicators.Clock:       C_('option-entry|indicators|name', 'Clock'),
        Indicators.Host:        C_('option-entry|indicators|name', 'Host name'),
        Indicators.Layout:      C_('option-entry|indicators|name', 'Keyboard layout'),
        Indicators.Session:     C_('option-entry|indicators|name', 'Sessions menu'),
        Indicators.Language:    C_('option-entry|indicators|name', 'Languages menu'),
        Indicators.A11y:        C_('option-entry|indicators|name', 'Accessibility menu'),
        Indicators.Power:       C_('option-entry|indicators|name', 'Power menu')}
    # Default icons for indicators to display in treeview
    Icons = {
        Indicators.A11y:        'preferences-desktop-accessibility',
        Indicators.Session:     'document-properties',
        Indicators.Power:       'system-shutdown'}
    Tooltips = {
        Indicators.Spacer:      C_('option-entry|indicators|tooltip', 'Spacer'),
        Indicators.Separator:   C_('option-entry|indicators|tooltip', 'Separator'),
        Indicators.Text:        C_('option-entry|indicators|tooltip', 'Custom text or/and image'),
        Indicators.Host:        C_('option-entry|indicators|tooltip', 'Host name'),
        Indicators.Clock:       C_('option-entry|indicators|tooltip', 'Clock'),
        Indicators.Layout:      C_('option-entry|indicators|tooltip', 'Layout indicator'),
        Indicators.Session:     C_('option-entry|indicators|tooltip',
                                   'Sessions menu (xfce, unity, gnome etc.)'),
        Indicators.Language:    C_('option-entry|indicators|tooltip', 'Languages menu'),
        Indicators.A11y:        C_('option-entry|indicators|tooltip', 'Accessibility menu'),
        Indicators.Power:       C_('option-entry|indicators|tooltip', 'Power menu')}
    # Default options for indicators
    DefaultOptions = {
        Indicators.External:    {Option.Text: None, Option.Image: None},
        Indicators.Spacer:      {Option.Layout: set()},
        Indicators.Separator:   {Option.Layout: set()},
        Indicators.Text:        {Option.Layout: {LayoutSet.Text}, Option.Text: None},
        Indicators.Host:        {Option.Layout: {LayoutSet.Text}, Option.Text: None},
        Indicators.Clock:       {Option.Layout: {LayoutSet.Text}, Option.Text: None},
        Indicators.Layout:      {Option.Layout: {LayoutSet.Text}, Option.Text: None},
        Indicators.Session:     {Option.Layout: {LayoutSet.Text, LayoutSet.Image},
                                 Option.Text: None, Option.Image: None},
        Indicators.Language:    {Option.Layout: {LayoutSet.Text}, Option.Text: None},
        Indicators.A11y:        {Option.Layout: {LayoutSet.Image}, Option.Image: None},
        Indicators.Power:       {Option.Layout: {LayoutSet.Image}, Option.Image: None}}

    def __init__(self, widgets):
        super().__init__(widgets)

        if get_greeter_version() < 0x020100:
            self._get_value = self._get_value_19
            self._on_button_release = self._on_button_release_19

        for k, v in self.DefaultOptions.items():
            v[Option.Name] = k

        self._treeview = widgets['treeview']
        self._selection = widgets['selection']
        self._state_renderer = widgets['state_renderer']
        self._state_column = widgets['state_column']
        self._add = widgets['add']
        self._remove = widgets['remove']
        self._up = widgets['up']
        self._down = widgets['down']
        self._tools = widgets['tools']
        self._model = widgets['model']
        self._widgets_to_disable = [self._treeview, widgets['toolbar']]
        self._properties_dialog = None
        self._row_menu = None
        self._tools_menu = None
        self._show_unused = False

        self._treeview.connect('key-press-event', self._on_key_press)
        self._treeview.connect('row-activated', self._on_row_activated)
        self._treeview.connect('button-release-event', self._on_button_release)
        self._selection.connect('changed', self._on_selection_changed)
        self._state_renderer.connect('toggled', self._on_state_toggled)

        self._add.connect('clicked', self._on_add_clicked)
        self._remove.connect('clicked', self._on_remove_clicked)
        self._up.connect('clicked', self._on_up_clicked)
        self._down.connect('clicked', self._on_down_clicked)
        self._tools.connect('clicked', self._on_tools_clicked)

        self._on_row_changed_id = self._model.connect('row-changed', self._on_model_changed)
        self._on_row_deleted_id = self._model.connect('row-deleted', self._on_model_changed)
        self._on_row_inserted_id = self._model.connect('row-inserted', self._on_model_row_inserted)
        self._on_rows_reordered_id = self._model.connect('rows-reordered', self._on_model_changed)

    def _on_model_changed(self, *unused):
        self._emit_changed()

    def _on_model_row_inserted(self, model, path, rowiter):
        # Do not emit 'changed' for uninitialized row (dragging rows)
        # It can cause calling get_value() for model with invalid values
        if model[rowiter][Row.Name] is not None:
            self._emit_changed()

    def _get_value(self):
        def fix_token(s):
            s = s.replace('"', r'\"')
            if any(c in s for c in string.whitespace):
                s = '"' + s + '"'
            return s

        items = []
        for row in self._model:
            if row[Row.HasState] and not row[Row.State]:
                continue

            options = deepcopy(row[Row.Options].data)
            name = options.pop(Option.Name)
            defaults = deepcopy(self.DefaultOptions[name])

            # text, image, layout=image-text -> text, image
            if options.get(Option.Layout) == {LayoutSet.Text, LayoutSet.Image}:
                del options[Option.Layout]

            for k in defaults.keys() & options.keys():
                if defaults[k] == options[k]:
                    del options[k]

            if Option.Layout in options:
                layout = options[Option.Layout]
                options[Option.Layout] = LayoutSet._to_string(layout)
                # text, layout=text -> layout=text
                if LayoutSet.Text in layout and options.get(Option.Text, self) is None:
                    del options[Option.Text]
                if LayoutSet.Image in layout and options.get(Option.Image, self) is None:
                    del options[Option.Image]

            # name=~text, text=value -> ~~value
            if name == Indicators.Text:
                name = '~~' + (options.pop(Option.Text, None) or '')
            elif name == Indicators.External:
                name = options.pop(Option.Path, None) or ''

            if not options:
                items.append(fix_token(name))
            else:
                values = (fix_token(k) + '=' + fix_token(v) if v else fix_token(k)
                          for k, v in sorted(options.items(), key=operator.itemgetter(0)))
                items.append(fix_token(name) + ': ' + ', '.join(values))
        return '; '.join(items)

    def _get_value_19(self):
        items = []
        for row in self._model:
            if row[Row.HasState] and not row[Row.State]:
                continue

            options = deepcopy(row[Row.Options].data)
            name = options.pop(Option.Name)

            # name=~text, text=value -> ~~value
            if name == Indicators.Text:
                name = '~~' + (options.pop(Option.Text, None) or '')
            elif name == Indicators.External:
                name = options.pop(Option.Path, None) or ''

            items.append(name)
        return ';'.join(items)

    def _set_value(self, value):
        with self._model.handler_block(self._on_row_deleted_id):
            self._model.clear()

        for options in self._read_options_string(value):
            name = options[Option.Name]

            if name.startswith('~~'):
                options.setdefault(Option.Text, name[2:])
                options[Option.Name] = Indicators.Text
                name = Indicators.Text
            elif name not in BuiltInIndicators:
                options.setdefault(Option.Path, name)
                options[Option.Name] = Indicators.External
                name = Indicators.External

            defaults = deepcopy(self.DefaultOptions[name])

            if Option.Markup in options:
                markup = options[Option.Markup]
                if markup is not None:
                    options[Option.Text] = markup
                options[Option.Markup] = None

            if Option.Layout in options:
                options[Option.Layout] = Layout._to_set(options[Option.Layout])
            else:
                options[Option.Layout] = defaults.get(Option.Layout) or set()

            if Option.Text in options:
                options[Option.Layout].add(LayoutSet.Text)
            elif LayoutSet.Text in options[Option.Layout]:
                options.setdefault(Option.Text, None)
            else:
                defaults.pop(Option.Text, None)

            if Option.Image in options:
                options[Option.Layout].add(LayoutSet.Image)
            elif LayoutSet.Image in options[Option.Layout]:
                options.setdefault(Option.Image, None)
            else:
                defaults.pop(Option.Image, None)

            options.update((k, defaults[k])
                           for k in defaults.keys() - options.keys())

            with self._model.handler_block(self._on_row_changed_id), \
                    self._model.handler_block(self._on_row_inserted_id):
                self._set_row(None, options, select=False)

        if self._show_unused:
            self._tools_show_unused_toggled()

        self._selection.select_path(0)
        self._on_model_changed()

    def _read_options_string(self, s):

        while s:
            name, s = self._next_string_token(s, ':;')
            if not name:
                continue
            options = {Option.Name: name}

            if s.startswith(':'):
                while s:
                    option, s = self._next_string_token(s[1:], '=,;')
                    if s.startswith('='):
                        value, s = self._next_string_token(s[1:], ',;')
                    else:
                        value = None
                    options[option] = value
                    if not s.startswith(','):
                        break

            yield options
            s = s[1:]

    def _next_string_token(self, s, delimiters):
        token = []
        quoted = False

        for last, c in enumerate(s):
            if not c.isspace():
                break

        # Parsing quotes
        for i, c in enumerate(s[last:], last):
            if c == '"':
                if i > last and s[i - 1] == '\\':
                    token.append(s[last:i - 1])
                    token.append('"')
                else:
                    token.append(s[last:i])
                    quoted = not quoted
                last = i + 1
            elif not quoted and c in delimiters:
                break

        if quoted:
            return '', ''

        if last != i or last == 0:
            token.append(s[last: i if c in delimiters else i + 1].rstrip())

        return ''.join(token) if token else None, s[i:]

    def _remove_selection(self):
        model, rowiter = self._selection.get_selected()
        if rowiter:
            if self._show_unused and model[rowiter][Row.HasState]:
                model[rowiter][Row.State] = False
            else:
                model.remove(rowiter)
            self._on_selection_changed()

    def _move_selection(self, move_up):
        model, rowiter = self._selection.get_selected()
        if rowiter:
            next_iter = model.iter_previous(
                rowiter) if move_up else model.iter_next(rowiter)
            if self._show_unused and \
               (model[rowiter][Row.HasState] and not model[rowiter][Row.State] or
                    model[next_iter][Row.HasState] and not model[next_iter][Row.State]):
                with self._model.handler_block(self._on_rows_reordered_id):
                    model.swap(rowiter, next_iter)
            else:
                model.swap(rowiter, next_iter)
            self._on_selection_changed()

    def _create_row_tuple(self, options):
        name = options[Option.Name]
        error = None

        text = options.get(Option.Text)
        if Option.Text in options:
            if text is not None:
                if Option.Markup in options:
                    error = get_markup_error(text)
                    if error:
                        text = '<i>{text}</i>'.format(text=escape_markup(text))
                else:
                    text = escape_markup(text)
                text = '"' + text + '"'
            elif name == Indicators.Host:
                text = escape_markup(platform.node())

        display_name = self.Names.get(name, name)
        if name == Indicators.External:
            if options.get(Option.Path):
                title = '{name} ({value})'.format(name=escape_markup(display_name),
                                                  value=escape_markup(options[Option.Path]))
            else:
                title = escape_markup(display_name)
        else:
            title = escape_markup(display_name)

        if text:
            markup = '{name}: {text}'.format(name=title, text=text)
        else:
            markup = title

        if Option.Image in options or get_greeter_version() < 0x020100:
            icon = options.get(Option.Image)
            if icon and icon.startswith('#'):
                icon = icon[1:]
            elif icon:
                icon = 'image-x-generic'
            else:
                if name in self.Icons:
                    icon = self.Icons[name]
                elif name in BuiltInIndicators:
                    icon = 'applications-system'
                else:
                    icon = 'application-x-executable'
        else:
            icon = ''

        has_state = name in SingleIndicators

        return Row._make(Name=name,
                         Tooltip=self.Tooltips.get(name),
                         Icon=icon,
                         Markup=markup,
                         HasState=has_state, State=has_state,
                         Options=TreeStoreDataWrapper(options))

    def _set_row(self, rowiter, options, select=True):
        old_name = self._model[rowiter][Row.Name] if rowiter else None
        new_name = options.get(
            Option.Name, '') if options is not None else None
        old_is_single = old_name in SingleIndicators
        new_is_single = new_name in SingleIndicators

        if new_name == old_name:
            # The same row - just update
            pass
        elif old_is_single and new_is_single:
            old_row = next(
                (row for row in self._model if row[Row.Name] == new_name), None)
            if old_row:
                if self._show_unused:
                    # Swap current row with new_row
                    with self._model.handler_block(self._on_rows_reordered_id):
                        self._model.move_before(old_row.iter, rowiter)
                    with self._model.handler_block(self._on_row_changed_id):
                        self._model[rowiter][Row.State] = False
                    rowiter = old_row.iter
                else:
                    # Replace current row with replace_row
                    with self._model.handler_block(self._on_row_deleted_id):
                        self._model.remove(old_row.iter)
        elif old_is_single:
            if self._show_unused:
                # Uncheck old row and use new instead of it
                with self._model.handler_block(self._on_row_changed_id):
                    self._model[rowiter][Row.State] = False
                with self._model.handler_block(self._on_row_inserted_id):
                    new_iter = self._model.insert_after(rowiter)
                rowiter = new_iter
        elif new_is_single:
            old_row = next(
                (row for row in self._model if row[Row.Name] == new_name), None)
            if old_row:
                with self._model.handler_block(self._on_row_deleted_id):
                    self._model.remove(old_row.iter)

        if rowiter and options:
            with self._model.handler_block(self._on_row_changed_id):
                self._model[rowiter] = self._create_row_tuple(options)
            self._model.row_changed(self._model.get_path(rowiter), rowiter)
        elif options:
            rowiter = self._model.append(self._create_row_tuple(options))

        if select and rowiter:
            self._selection.select_iter(rowiter)

        return rowiter

    def _edit_indicator(self, options, add_callback=None):
        if not self._properties_dialog:
            from lightdm_gtk_greeter_settings.IndicatorPropertiesDialog \
                import IndicatorPropertiesDialog as Dialog
            self._properties_dialog = Dialog(is_duplicate=self._is_duplicate,
                                             get_defaults=self.DefaultOptions.get,
                                             get_name=self.Names.get)
            self._properties_dialog.props.transient_for = self._treeview.get_toplevel()

        self._properties_dialog.add_callback = add_callback
        self._properties_dialog.set_indicator(options)
        if self._properties_dialog.run() == Gtk.ResponseType.OK:
            options = self._properties_dialog.get_indicator()
        else:
            options = None
        self._properties_dialog.hide()
        return options

    def _is_duplicate(self, name):
        return name in SingleIndicators and any(row[Row.Name] == name
                                                for row in self._model if row[Row.State])

    def _add_indicator(self, options):
        self._set_row(None, options)

    def _on_key_press(self, treeview, event):
        if Gdk.keyval_name(event.keyval) == 'Delete':
            self._remove_selection()
        elif Gdk.keyval_name(event.keyval) == 'F2':
            model, rowiter = self._selection.get_selected()
            treeview.row_activated(model.get_path(rowiter), None)
        else:
            return False
        return True

    def _on_row_activated(self, treeview, path, column):
        if column != self._state_column:
            options = self._edit_indicator(self._model[path][Row.Options].data)
            if options:
                self._set_row(self._model.get_iter(path), options)

    def _on_button_release(self, treeview, event):
        if event.button != 3:
            return False

        pos = treeview.get_path_at_pos(int(event.x), int(event.y))
        if not pos:
            return False

        row = self._model[pos[0]]
        if row[Row.HasState] and not row[Row.State]:
            return False

        if not self._row_menu:
            self._row_menu = Gtk.Menu()
            self._row_menu_reset = Gtk.MenuItem(C_('option-entry|indicators',
                                                   'Reset to _defaults'))
            self._row_menu_text = Gtk.CheckMenuItem(C_('option-entry|indicators',
                                                       'Display _label'))
            self._row_menu_image = Gtk.CheckMenuItem(C_('option-entry|indicators',
                                                        'Display _image'))
            self._row_menu_remove = Gtk.MenuItem(
                C_('option-entry|indicators', '_Remove'))

            self._row_menu_text_id = self._row_menu_text.connect('toggled',
                                                                 self._on_row_menu_toggled,
                                                                 Option.Text)
            self._row_menu_image_id = self._row_menu_image.connect('toggled',
                                                                   self._on_row_menu_toggled,
                                                                   Option.Image)
            self._row_menu_reset.connect(
                'activate', self._on_row_menu_reset_clicked)
            self._row_menu_remove.connect('activate', self._on_remove_clicked)

            self._row_menu.append(self._row_menu_reset)
            self._row_menu.append(self._row_menu_text)
            self._row_menu.append(self._row_menu_image)
            self._row_menu.append(Gtk.SeparatorMenuItem())
            self._row_menu.append(self._row_menu_remove)

            for item in self._row_menu:
                if type(item) is not Gtk.SeparatorMenuItem:
                    item.props.use_underline = True
                item.props.visible = True

        options = row[Row.Options].data

        with self._row_menu_text.handler_block(self._row_menu_text_id):
            self._row_menu_text.props.active = Option.Text in options
        with self._row_menu_image.handler_block(self._row_menu_image_id):
            self._row_menu_image.props.active = Option.Image in options

        editable = options[Option.Name] not in {
            Indicators.Spacer, Indicators.Separator}
        self._row_menu_reset.props.sensitive = editable
        self._row_menu_text.props.sensitive = editable
        self._row_menu_image.props.sensitive = editable

        self._row_menu.popup(None, None, None, None, 0,
                             Gtk.get_current_event_time())

        return True

    def _on_button_release_19(self, treeview, event):
        pass

    def _on_row_menu_reset_clicked(self, item):
        model, rowiter = self._selection.get_selected()
        if rowiter:
            name = model[rowiter][Row.Name]
            options = deepcopy(self.DefaultOptions[name])
            options[Option.Name] = name
            with model.handler_block(self._on_row_changed_id):
                model[rowiter] = self._create_row_tuple(options)
            model.row_changed(model.get_path(rowiter), rowiter)

    def _on_row_menu_toggled(self, item, option):
        model, rowiter = self._selection.get_selected()
        options = model[rowiter][Row.Options].data
        if item.props.active:
            options.setdefault(option, None)
        else:
            options.pop(option, None)
        model[rowiter] = self._create_row_tuple(options)

    def _on_state_toggled(self, renderer, path):
        self._model[path][Row.State] = not self._model[path][Row.State]

    def _on_selection_changed(self, selection=None):
        model, rowiter = self._selection.get_selected()
        if rowiter:
            row = model[rowiter]
            self._remove.props.sensitive = not row[
                Row.HasState] or row[Row.State]
            self._down.props.sensitive = model.iter_next(rowiter)
            self._up.props.sensitive = model.iter_previous(rowiter)
            self._treeview.scroll_to_cell(model.get_path(rowiter))
        else:
            self._remove.props.sensitive = False
            self._down.props.sensitive = False
            self._up.props.sensitive = False

    def _on_add_clicked(self, button=None):
        options = self._edit_indicator(self.DefaultOptions[Indicators.External],
                                       add_callback=self._add_indicator)
        if options:
            self._set_row(None, options, select=True)

    def _on_remove_clicked(self, button=None):
        self._remove_selection()

    def _on_up_clicked(self, button=None):
        self._move_selection(move_up=True)

    def _on_down_clicked(self, button=None):
        self._move_selection(move_up=False)

    def _on_tools_clicked(self, button=None):
        if not self._tools_menu:
            self._tools_menu = Gtk.Menu()
            self._tools_menu.attach_to_widget(self._tools)

            unused_item = Gtk.CheckMenuItem(C_('option-entry|indicators', 'Show unused items'))
            unused_item.connect('toggled', self._tools_show_unused_toggled)
            self._tools_menu.append(unused_item)

            header_item = Gtk.MenuItem(C_('option-entry|indicators', 'Predefined templates:'))
            header_item.props.sensitive = False
            self._tools_menu.append(Gtk.SeparatorMenuItem())
            self._tools_menu.append(header_item)

            templates = (
                ('host ~ clock, language, session, power',
                 '~host;~spacer;~language;~session;~power'),
                ('host ~ clock ~ language, session, a11y, power',
                 '~host;~spacer;~clock;~spacer;~language;~session;~a11y;~power'),
                ('host, layout, clock ~ language, session, power',
                 '~host;~layout;~clock;~spacer;~language;~session;~power'))

            for title, value in templates:
                item = Gtk.MenuItem(title)
                item.connect('activate', self._on_tools_template_clicked, value)
                self._tools_menu.append(item)

            self._tools_menu.show_all()
        self._tools_menu.popup(None, None, None, None, 0,
                               Gtk.get_current_event_time())

    def _on_tools_template_clicked(self, item, value):
        self._set_value(value)

    def _tools_show_unused_toggled(self, widget=None):
        if widget:
            self._show_unused = widget.props.active
        self._state_column.props.visible = self._show_unused

        used = {row[Row.Name]: row
                for row in self._model if row[Row.Name] in SingleIndicators}
        if self._show_unused:
            for name in SingleIndicators - used.keys():
                options = deepcopy(self.DefaultOptions[name])
                options[Option.Name] = name
                with self._model.handler_block(self._on_row_changed_id),\
                        self._model.handler_block(self._on_row_inserted_id):
                    rowiter = self._set_row(None, options, select=False)
                    self._model[rowiter][Row.State] = False
        else:
            for row in used.values():
                if row[Row.HasState] and not row[Row.State]:
                    with self._model.handler_block(self._on_row_deleted_id):
                        self._model.remove(row.iter)
Exemplo n.º 4
0
class IndicatorsEntry(BaseEntry):
    # Readable names for indicators
    Names = {
        Indicators.External:
        C_('option-entry|indicators|name', 'External library/service'),
        Indicators.Spacer:
        C_('option-entry|indicators|name', 'Spacer'),
        Indicators.Separator:
        C_('option-entry|indicators|name', 'Separator'),
        Indicators.Text:
        C_('option-entry|indicators|name', 'Text'),
        Indicators.Clock:
        C_('option-entry|indicators|name', 'Clock'),
        Indicators.Host:
        C_('option-entry|indicators|name', 'Host name'),
        Indicators.Layout:
        C_('option-entry|indicators|name', 'Keyboard layout'),
        Indicators.Session:
        C_('option-entry|indicators|name', 'Sessions menu'),
        Indicators.Language:
        C_('option-entry|indicators|name', 'Languages menu'),
        Indicators.A11y:
        C_('option-entry|indicators|name', 'Accessibility menu'),
        Indicators.Power:
        C_('option-entry|indicators|name', 'Power menu')
    }
    # Default icons for indicators to display in treeview
    Icons = {
        Indicators.A11y: 'preferences-desktop-accessibility',
        Indicators.Session: 'document-properties',
        Indicators.Power: 'system-shutdown'
    }
    Tooltips = {
        Indicators.Spacer:
        C_('option-entry|indicators|tooltip', 'Spacer'),
        Indicators.Separator:
        C_('option-entry|indicators|tooltip', 'Separator'),
        Indicators.Text:
        C_('option-entry|indicators|tooltip', 'Custom text or/and image'),
        Indicators.Host:
        C_('option-entry|indicators|tooltip', 'Host name'),
        Indicators.Clock:
        C_('option-entry|indicators|tooltip', 'Clock'),
        Indicators.Layout:
        C_('option-entry|indicators|tooltip', 'Layout indicator'),
        Indicators.Session:
        C_('option-entry|indicators|tooltip',
           'Sessions menu (xfce, unity, gnome etc.)'),
        Indicators.Language:
        C_('option-entry|indicators|tooltip', 'Languages menu'),
        Indicators.A11y:
        C_('option-entry|indicators|tooltip', 'Accessibility menu'),
        Indicators.Power:
        C_('option-entry|indicators|tooltip', 'Power menu')
    }
    # Default options for indicators
    DefaultOptions = {
        Indicators.External: {
            Option.Text: None,
            Option.Image: None
        },
        Indicators.Spacer: {
            Option.Layout: set()
        },
        Indicators.Separator: {
            Option.Layout: set()
        },
        Indicators.Text: {
            Option.Layout: {LayoutSet.Text},
            Option.Text: None
        },
        Indicators.Host: {
            Option.Layout: {LayoutSet.Text},
            Option.Text: None
        },
        Indicators.Clock: {
            Option.Layout: {LayoutSet.Text},
            Option.Text: None
        },
        Indicators.Layout: {
            Option.Layout: {LayoutSet.Text},
            Option.Text: None
        },
        Indicators.Session: {
            Option.Layout: {LayoutSet.Text, LayoutSet.Image},
            Option.Text: None,
            Option.Image: None
        },
        Indicators.Language: {
            Option.Layout: {LayoutSet.Text},
            Option.Text: None
        },
        Indicators.A11y: {
            Option.Layout: {LayoutSet.Image},
            Option.Image: None
        },
        Indicators.Power: {
            Option.Layout: {LayoutSet.Image},
            Option.Image: None
        }
    }

    def __init__(self, widgets):
        super().__init__(widgets)

        if get_greeter_version() < 0x020100:
            self._get_value = self._get_value_19
            self._on_button_release = self._on_button_release_19

        for k, v in self.DefaultOptions.items():
            v[Option.Name] = k

        self._treeview = widgets['treeview']
        self._selection = widgets['selection']
        self._state_renderer = widgets['state_renderer']
        self._state_column = widgets['state_column']
        self._add = widgets['add']
        self._remove = widgets['remove']
        self._up = widgets['up']
        self._down = widgets['down']
        self._tools = widgets['tools']
        self._model = widgets['model']
        self._widgets_to_disable = [self._treeview, widgets['toolbar']]
        self._properties_dialog = None
        self._row_menu = None
        self._tools_menu = None
        self._show_unused = False

        self._treeview.connect('key-press-event', self._on_key_press)
        self._treeview.connect('row-activated', self._on_row_activated)
        self._treeview.connect('button-release-event', self._on_button_release)
        self._selection.connect('changed', self._on_selection_changed)
        self._state_renderer.connect('toggled', self._on_state_toggled)

        self._add.connect('clicked', self._on_add_clicked)
        self._remove.connect('clicked', self._on_remove_clicked)
        self._up.connect('clicked', self._on_up_clicked)
        self._down.connect('clicked', self._on_down_clicked)
        self._tools.connect('clicked', self._on_tools_clicked)

        self._on_row_changed_id = self._model.connect('row-changed',
                                                      self._on_model_changed)
        self._on_row_deleted_id = self._model.connect('row-deleted',
                                                      self._on_model_changed)
        self._on_row_inserted_id = self._model.connect(
            'row-inserted', self._on_model_row_inserted)
        self._on_rows_reordered_id = self._model.connect(
            'rows-reordered', self._on_model_changed)

    def _on_model_changed(self, *unused):
        self._emit_changed()

    def _on_model_row_inserted(self, model, path, rowiter):
        # Do not emit 'changed' for uninitialized row (dragging rows)
        # It can cause calling get_value() for model with invalid values
        if model[rowiter][Row.Name] is not None:
            self._emit_changed()

    def _get_value(self):
        def fix_token(s):
            s = s.replace('"', r'\"')
            if any(c in s for c in string.whitespace):
                s = '"' + s + '"'
            return s

        items = []
        for row in self._model:
            if row[Row.HasState] and not row[Row.State]:
                continue

            options = deepcopy(row[Row.Options].data)
            name = options.pop(Option.Name)
            defaults = deepcopy(self.DefaultOptions[name])

            # text, image, layout=image-text -> text, image
            if options.get(Option.Layout) == {LayoutSet.Text, LayoutSet.Image}:
                del options[Option.Layout]

            for k in defaults.keys() & options.keys():
                if defaults[k] == options[k]:
                    del options[k]

            if Option.Layout in options:
                layout = options[Option.Layout]
                options[Option.Layout] = LayoutSet._to_string(layout)
                # text, layout=text -> layout=text
                if LayoutSet.Text in layout and options.get(Option.Text,
                                                            self) is None:
                    del options[Option.Text]
                if LayoutSet.Image in layout and options.get(
                        Option.Image, self) is None:
                    del options[Option.Image]

            # name=~text, text=value -> ~~value
            if name == Indicators.Text:
                name = '~~' + (options.pop(Option.Text, None) or '')
            elif name == Indicators.External:
                name = options.pop(Option.Path, None) or ''

            if not options:
                items.append(fix_token(name))
            else:
                values = (fix_token(k) + '=' +
                          fix_token(v) if v else fix_token(k)
                          for k, v in sorted(options.items(),
                                             key=operator.itemgetter(0)))
                items.append(fix_token(name) + ': ' + ', '.join(values))
        return '; '.join(items)

    def _get_value_19(self):
        items = []
        for row in self._model:
            if row[Row.HasState] and not row[Row.State]:
                continue

            options = deepcopy(row[Row.Options].data)
            name = options.pop(Option.Name)

            # name=~text, text=value -> ~~value
            if name == Indicators.Text:
                name = '~~' + (options.pop(Option.Text, None) or '')
            elif name == Indicators.External:
                name = options.pop(Option.Path, None) or ''

            items.append(name)
        return ';'.join(items)

    def _set_value(self, value):
        with self._model.handler_block(self._on_row_deleted_id):
            self._model.clear()

        for options in self._read_options_string(value):
            name = options[Option.Name]

            if name.startswith('~~'):
                options.setdefault(Option.Text, name[2:])
                options[Option.Name] = Indicators.Text
                name = Indicators.Text
            elif name not in BuiltInIndicators:
                options.setdefault(Option.Path, name)
                options[Option.Name] = Indicators.External
                name = Indicators.External

            defaults = deepcopy(self.DefaultOptions[name])

            if Option.Markup in options:
                markup = options[Option.Markup]
                if markup is not None:
                    options[Option.Text] = markup
                options[Option.Markup] = None

            if Option.Layout in options:
                options[Option.Layout] = Layout._to_set(options[Option.Layout])
            else:
                options[Option.Layout] = defaults.get(Option.Layout) or set()

            if Option.Text in options:
                options[Option.Layout].add(LayoutSet.Text)
            elif LayoutSet.Text in options[Option.Layout]:
                options.setdefault(Option.Text, None)
            else:
                defaults.pop(Option.Text, None)

            if Option.Image in options:
                options[Option.Layout].add(LayoutSet.Image)
            elif LayoutSet.Image in options[Option.Layout]:
                options.setdefault(Option.Image, None)
            else:
                defaults.pop(Option.Image, None)

            options.update(
                (k, defaults[k]) for k in defaults.keys() - options.keys())

            with self._model.handler_block(self._on_row_changed_id), \
                    self._model.handler_block(self._on_row_inserted_id):
                self._set_row(None, options, select=False)

        if self._show_unused:
            self._tools_show_unused_toggled()

        self._selection.select_path(0)
        self._on_model_changed()

    def _read_options_string(self, s):
        while s:
            name, s = self._next_string_token(s, ':;')
            options = {Option.Name: name}

            if s.startswith(':'):
                while s:
                    option, s = self._next_string_token(s[1:], '=,;')
                    if s.startswith('='):
                        value, s = self._next_string_token(s[1:], ',;')
                    else:
                        value = None
                    options[option] = value
                    if not s.startswith(','):
                        break

            yield options
            s = s[1:]

    def _next_string_token(self, s, delimiters):
        token = []
        quoted = False

        for last, c in enumerate(s):
            if not c.isspace():
                break

        # Parsing quotes
        for i, c in enumerate(s[last:], last):
            if c == '"':
                if i > last and s[i - 1] == '\\':
                    token.append(s[last:i - 1])
                    token.append('"')
                else:
                    token.append(s[last:i])
                    quoted = not quoted
                last = i + 1
            elif not quoted and c in delimiters:
                break

        if quoted:
            return '', ''

        if last != i or last == 0:
            token.append(s[last:i if c in delimiters else i + 1].rstrip())

        return ''.join(token) if token else None, s[i:]

    def _remove_selection(self):
        model, rowiter = self._selection.get_selected()
        if rowiter:
            if self._show_unused and model[rowiter][Row.HasState]:
                model[rowiter][Row.State] = False
            else:
                model.remove(rowiter)
            self._on_selection_changed()

    def _move_selection(self, move_up):
        model, rowiter = self._selection.get_selected()
        if rowiter:
            next_iter = model.iter_previous(
                rowiter) if move_up else model.iter_next(rowiter)
            if self._show_unused and \
               (model[rowiter][Row.HasState] and not model[rowiter][Row.State] or
                    model[next_iter][Row.HasState] and not model[next_iter][Row.State]):
                with self._model.handler_block(self._on_rows_reordered_id):
                    model.swap(rowiter, next_iter)
            else:
                model.swap(rowiter, next_iter)
            self._on_selection_changed()

    def _create_row_tuple(self, options):
        name = options[Option.Name]
        error = None

        text = options.get(Option.Text)
        if Option.Text in options:
            if text is not None:
                if Option.Markup in options:
                    error = get_markup_error(text)
                    if error:
                        text = '<i>{text}</i>'.format(text=escape_markup(text))
                else:
                    text = escape_markup(text)
                text = '"' + text + '"'
            elif name == Indicators.Host:
                text = escape_markup(platform.node())

        display_name = self.Names.get(name, name)
        if name == Indicators.External:
            if options.get(Option.Path):
                title = '{name} ({value})'.format(
                    name=escape_markup(display_name),
                    value=escape_markup(options[Option.Path]))
            else:
                title = escape_markup(display_name)
        else:
            title = escape_markup(display_name)

        if text:
            markup = '{name}: {text}'.format(name=title, text=text)
        else:
            markup = title

        if Option.Image in options or get_greeter_version() < 0x020100:
            icon = options.get(Option.Image)
            if icon and icon.startswith('#'):
                icon = icon[1:]
            elif icon:
                icon = 'image-x-generic'
            else:
                if name in self.Icons:
                    icon = self.Icons[name]
                elif name in BuiltInIndicators:
                    icon = 'applications-system'
                else:
                    icon = 'application-x-executable'
        else:
            icon = ''

        has_state = name in SingleIndicators

        return Row._make(Name=name,
                         Tooltip=self.Tooltips.get(name),
                         Icon=icon,
                         Markup=markup,
                         HasState=has_state,
                         State=has_state,
                         Options=TreeStoreDataWrapper(options))

    def _set_row(self, rowiter, options, select=True):
        old_name = self._model[rowiter][Row.Name] if rowiter else None
        new_name = options.get(Option.Name,
                               '') if options is not None else None
        old_is_single = old_name in SingleIndicators
        new_is_single = new_name in SingleIndicators

        if new_name == old_name:
            # The same row - just update
            pass
        elif old_is_single and new_is_single:
            old_row = next(
                (row for row in self._model if row[Row.Name] == new_name),
                None)
            if old_row:
                if self._show_unused:
                    # Swap current row with new_row
                    with self._model.handler_block(self._on_rows_reordered_id):
                        self._model.move_before(old_row.iter, rowiter)
                    with self._model.handler_block(self._on_row_changed_id):
                        self._model[rowiter][Row.State] = False
                    rowiter = old_row.iter
                else:
                    # Replace current row with replace_row
                    with self._model.handler_block(self._on_row_deleted_id):
                        self._model.remove(old_row.iter)
        elif old_is_single:
            if self._show_unused:
                # Uncheck old row and use new instead of it
                with self._model.handler_block(self._on_row_changed_id):
                    self._model[rowiter][Row.State] = False
                with self._model.handler_block(self._on_row_inserted_id):
                    new_iter = self._model.insert_after(rowiter)
                rowiter = new_iter
        elif new_is_single:
            old_row = next(
                (row for row in self._model if row[Row.Name] == new_name),
                None)
            if old_row:
                with self._model.handler_block(self._on_row_deleted_id):
                    self._model.remove(old_row.iter)

        if rowiter and options:
            with self._model.handler_block(self._on_row_changed_id):
                self._model[rowiter] = self._create_row_tuple(options)
            self._model.row_changed(self._model.get_path(rowiter), rowiter)
        elif options:
            rowiter = self._model.append(self._create_row_tuple(options))

        if select and rowiter:
            self._selection.select_iter(rowiter)

        return rowiter

    def _edit_indicator(self, options, add_callback=None):
        if not self._properties_dialog:
            from lightdm_gtk_greeter_settings.IndicatorPropertiesDialog \
                import IndicatorPropertiesDialog as Dialog
            self._properties_dialog = Dialog(
                is_duplicate=self._is_duplicate,
                get_defaults=self.DefaultOptions.get,
                get_name=self.Names.get)
            self._properties_dialog.props.transient_for = self._treeview.get_toplevel(
            )

        self._properties_dialog.add_callback = add_callback
        self._properties_dialog.set_indicator(options)
        if self._properties_dialog.run() == Gtk.ResponseType.OK:
            options = self._properties_dialog.get_indicator()
        else:
            options = None
        self._properties_dialog.hide()
        return options

    def _is_duplicate(self, name):
        return name in SingleIndicators and any(
            row[Row.Name] == name for row in self._model if row[Row.State])

    def _add_indicator(self, options):
        self._set_row(None, options)

    def _on_key_press(self, treeview, event):
        if Gdk.keyval_name(event.keyval) == 'Delete':
            self._remove_selection()
        elif Gdk.keyval_name(event.keyval) == 'F2':
            model, rowiter = self._selection.get_selected()
            treeview.row_activated(model.get_path(rowiter), None)
        else:
            return False
        return True

    def _on_row_activated(self, treeview, path, column):
        if column != self._state_column:
            options = self._edit_indicator(self._model[path][Row.Options].data)
            if options:
                self._set_row(self._model.get_iter(path), options)

    def _on_button_release(self, treeview, event):
        if event.button != 3:
            return False

        pos = treeview.get_path_at_pos(int(event.x), int(event.y))
        if not pos:
            return False

        row = self._model[pos[0]]
        if row[Row.HasState] and not row[Row.State]:
            return False

        if not self._row_menu:
            self._row_menu = Gtk.Menu()
            self._row_menu_reset = Gtk.MenuItem(
                C_('option-entry|indicators', 'Reset to _defaults'))
            self._row_menu_text = Gtk.CheckMenuItem(
                C_('option-entry|indicators', 'Display _label'))
            self._row_menu_image = Gtk.CheckMenuItem(
                C_('option-entry|indicators', 'Display _image'))
            self._row_menu_remove = Gtk.MenuItem(
                C_('option-entry|indicators', '_Remove'))

            self._row_menu_text_id = self._row_menu_text.connect(
                'toggled', self._on_row_menu_toggled, Option.Text)
            self._row_menu_image_id = self._row_menu_image.connect(
                'toggled', self._on_row_menu_toggled, Option.Image)
            self._row_menu_reset.connect('activate',
                                         self._on_row_menu_reset_clicked)
            self._row_menu_remove.connect('activate', self._on_remove_clicked)

            self._row_menu.append(self._row_menu_reset)
            self._row_menu.append(self._row_menu_text)
            self._row_menu.append(self._row_menu_image)
            self._row_menu.append(Gtk.SeparatorMenuItem())
            self._row_menu.append(self._row_menu_remove)

            for item in self._row_menu:
                if type(item) is not Gtk.SeparatorMenuItem:
                    item.props.use_underline = True
                item.props.visible = True

        options = row[Row.Options].data

        with self._row_menu_text.handler_block(self._row_menu_text_id):
            self._row_menu_text.props.active = Option.Text in options
        with self._row_menu_image.handler_block(self._row_menu_image_id):
            self._row_menu_image.props.active = Option.Image in options

        editable = options[Option.Name] not in {
            Indicators.Spacer, Indicators.Separator
        }
        self._row_menu_reset.props.sensitive = editable
        self._row_menu_text.props.sensitive = editable
        self._row_menu_image.props.sensitive = editable

        self._row_menu.popup(None, None, None, None, 0,
                             Gtk.get_current_event_time())

        return True

    def _on_button_release_19(self, treeview, event):
        pass

    def _on_row_menu_reset_clicked(self, item):
        model, rowiter = self._selection.get_selected()
        if rowiter:
            name = model[rowiter][Row.Name]
            options = deepcopy(self.DefaultOptions[name])
            options[Option.Name] = name
            with model.handler_block(self._on_row_changed_id):
                model[rowiter] = self._create_row_tuple(options)
            model.row_changed(model.get_path(rowiter), rowiter)

    def _on_row_menu_toggled(self, item, option):
        model, rowiter = self._selection.get_selected()
        options = model[rowiter][Row.Options].data
        if item.props.active:
            options.setdefault(option, None)
        else:
            options.pop(option, None)
        model[rowiter] = self._create_row_tuple(options)

    def _on_state_toggled(self, renderer, path):
        self._model[path][Row.State] = not self._model[path][Row.State]

    def _on_selection_changed(self, selection=None):
        model, rowiter = self._selection.get_selected()
        if rowiter:
            row = model[rowiter]
            self._remove.props.sensitive = not row[Row.HasState] or row[
                Row.State]
            self._down.props.sensitive = model.iter_next(rowiter)
            self._up.props.sensitive = model.iter_previous(rowiter)
            self._treeview.scroll_to_cell(model.get_path(rowiter))
        else:
            self._remove.props.sensitive = False
            self._down.props.sensitive = False
            self._up.props.sensitive = False

    def _on_add_clicked(self, button=None):
        options = self._edit_indicator(
            self.DefaultOptions[Indicators.External],
            add_callback=self._add_indicator)
        if options:
            self._set_row(None, options, select=True)

    def _on_remove_clicked(self, button=None):
        self._remove_selection()

    def _on_up_clicked(self, button=None):
        self._move_selection(move_up=True)

    def _on_down_clicked(self, button=None):
        self._move_selection(move_up=False)

    def _on_tools_clicked(self, button=None):
        if not self._tools_menu:
            self._tools_menu = Gtk.Menu()
            self._tools_menu.attach_to_widget(self._tools)

            unused_item = Gtk.CheckMenuItem(
                C_('option-entry|indicators', 'Show unused items'))
            unused_item.connect('toggled', self._tools_show_unused_toggled)
            self._tools_menu.append(unused_item)

            header_item = Gtk.MenuItem(
                C_('option-entry|indicators', 'Predefined templates:'))
            header_item.props.sensitive = False
            self._tools_menu.append(Gtk.SeparatorMenuItem())
            self._tools_menu.append(header_item)

            templates = (
                ('host ~ clock, language, session, power',
                 '~host;~spacer;~language;~session;~power'),
                ('host ~ clock ~ language, session, a11y, power',
                 '~host;~spacer;~clock;~spacer;~language;~session;~a11y;~power'
                 ), ('host, layout, clock ~ language, session, power',
                     '~host;~layout;~clock;~spacer;~language;~session;~power'))

            for title, value in templates:
                item = Gtk.MenuItem(title)
                item.connect('activate', self._on_tools_template_clicked,
                             value)
                self._tools_menu.append(item)

            self._tools_menu.show_all()
        self._tools_menu.popup(None, None, None, None, 0,
                               Gtk.get_current_event_time())

    def _on_tools_template_clicked(self, item, value):
        self._set_value(value)

    def _tools_show_unused_toggled(self, widget=None):
        if widget:
            self._show_unused = widget.props.active
        self._state_column.props.visible = self._show_unused

        used = {
            row[Row.Name]: row
            for row in self._model if row[Row.Name] in SingleIndicators
        }
        if self._show_unused:
            for name in SingleIndicators - used.keys():
                options = deepcopy(self.DefaultOptions[name])
                options[Option.Name] = name
                with self._model.handler_block(self._on_row_changed_id),\
                        self._model.handler_block(self._on_row_inserted_id):
                    rowiter = self._set_row(None, options, select=False)
                    self._model[rowiter][Row.State] = False
        else:
            for row in used.values():
                if row[Row.HasState] and not row[Row.State]:
                    with self._model.handler_block(self._on_row_deleted_id):
                        self._model.remove(row.iter)