# Copyright (C) 2018 Philipp Hörist # # This file is part of Gajim. # # Gajim 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; version 3 only. # # Gajim 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 Gajim. If not, see . from collections import namedtuple from enum import IntEnum from gi.repository import Gtk from gi.repository import Gdk from gajim.common import app from gajim.common.i18n import _ from gajim.common.const import StyleAttr, DialogButton, ButtonAction from gajim.common.connection_handlers_events import StyleChanged from gajim.gtk.dialogs import ErrorDialog from gajim.gtk.dialogs import NewConfirmationDialog from gajim.gtk.util import get_builder StyleOption = namedtuple('StyleOption', 'label selector attr') CSS_STYLE_OPTIONS = [ StyleOption(_('Chatstate Composing'), '.gajim-state-composing', StyleAttr.COLOR), StyleOption(_('Chatstate Inactive'), '.gajim-state-inactive', StyleAttr.COLOR), StyleOption(_('Chatstate Gone'), '.gajim-state-gone', StyleAttr.COLOR), StyleOption(_('Chatstate Paused'), '.gajim-state-paused', StyleAttr.COLOR), StyleOption(_('MUC Tab New Directed Message'), '.gajim-state-tab-muc-directed-msg', StyleAttr.COLOR), StyleOption(_('MUC Tab New Message'), '.gajim-state-tab-muc-msg', StyleAttr.COLOR), StyleOption(_('Banner Foreground Color'), '.gajim-banner', StyleAttr.COLOR), StyleOption(_('Banner Background Color'), '.gajim-banner', StyleAttr.BACKGROUND), StyleOption(_('Banner Font'), '.gajim-banner', StyleAttr.FONT), StyleOption(_('Account Row Foreground Color'), '.gajim-account-row', StyleAttr.COLOR), StyleOption(_('Account Row Background Color'), '.gajim-account-row', StyleAttr.BACKGROUND), StyleOption(_('Account Row Font'), '.gajim-account-row', StyleAttr.FONT), StyleOption(_('Group Row Foreground Color'), '.gajim-group-row', StyleAttr.COLOR), StyleOption(_('Group Row Background Color'), '.gajim-group-row', StyleAttr.BACKGROUND), StyleOption(_('Group Row Font'), '.gajim-group-row', StyleAttr.FONT), StyleOption(_('Contact Row Foreground Color'), '.gajim-contact-row', StyleAttr.COLOR), StyleOption(_('Contact Row Background Color'), '.gajim-contact-row', StyleAttr.BACKGROUND), StyleOption(_('Contact Row Font'), '.gajim-contact-row', StyleAttr.FONT), StyleOption(_('Conversation Font'), '.gajim-conversation-font', StyleAttr.FONT), StyleOption(_('Incoming Nickname Color'), '.gajim-incoming-nickname', StyleAttr.COLOR), StyleOption(_('Outgoing Nickname Color'), '.gajim-outgoing-nickname', StyleAttr.COLOR), StyleOption(_('Incoming Message Text Color'), '.gajim-incoming-message-text', StyleAttr.COLOR), StyleOption(_('Incoming Message Text Font'), '.gajim-incoming-message-text', StyleAttr.FONT), StyleOption(_('Outgoing Message Text Color'), '.gajim-outgoing-message-text', StyleAttr.COLOR), StyleOption(_('Outgoing Message Text Font'), '.gajim-outgoing-message-text', StyleAttr.FONT), StyleOption(_('Status Message Color'), '.gajim-status-message', StyleAttr.COLOR), StyleOption(_('Status Message Font'), '.gajim-status-message', StyleAttr.FONT), StyleOption(_('URL Color'), '.gajim-url', StyleAttr.COLOR), StyleOption(_('Highlight Message Color'), '.gajim-highlight-message', StyleAttr.COLOR), StyleOption(_('Message Correcting'), '.gajim-msg-correcting text', StyleAttr.BACKGROUND), StyleOption(_('Restored Message Color'), '.gajim-restored-message', StyleAttr.COLOR), StyleOption(_('Contact Disconnected Background'), '.gajim-roster-disconnected', StyleAttr.BACKGROUND), StyleOption(_('Contact Connected Background '), '.gajim-roster-connected', StyleAttr.BACKGROUND), ] class Column(IntEnum): THEME = 0 class Themes(Gtk.ApplicationWindow): def __init__(self, transient): Gtk.Window.__init__(self) self.set_application(app.app) self.set_title(_('Gajim Themes')) self.set_name('ThemesWindow') self.set_show_menubar(False) self.set_type_hint(Gdk.WindowTypeHint.DIALOG) self.set_transient_for(transient) self.set_resizable(True) self.set_default_size(600, 400) self.builder = get_builder('themes_window.ui') self.add(self.builder.get_object('theme_grid')) widgets = ['option_listbox', 'remove_theme_button', 'theme_store', 'theme_treeview', 'choose_option_listbox', 'add_option_button'] for widget in widgets: setattr(self, '_%s' % widget, self.builder.get_object(widget)) # Doesnt work if we set it in Glade self._add_option_button.set_sensitive(False) self._get_themes() self.builder.connect_signals(self) self.connect('destroy', self._on_destroy) self.show_all() self._fill_choose_listbox() def _get_themes(self): for theme in app.css_config.themes: self._theme_store.append([theme]) def _on_theme_name_edit(self, renderer, path, new_name): iter_ = self._theme_store.get_iter(path) old_name = self._theme_store[iter_][Column.THEME] if new_name == 'default': ErrorDialog( _('Invalid Name'), _('Name default is not allowed'), transient_for=self) return if ' ' in new_name: ErrorDialog( _('Invalid Name'), _('Spaces are not allowed'), transient_for=self) return if new_name == '': return result = app.css_config.rename_theme(old_name, new_name) if result is False: return self._theme_store.set_value(iter_, Column.THEME, new_name) def _select_theme_row(self, iter_): self._theme_treeview.get_selection().select_iter(iter_) def _on_theme_selected(self, tree_selection): store, iter_ = tree_selection.get_selected() if iter_ is None: self._clear_options() return theme = store[iter_][Column.THEME] app.css_config.change_preload_theme(theme) self._add_option_button.set_sensitive(True) self._remove_theme_button.set_sensitive(True) self._load_options(theme) def _load_options(self, name): self._option_listbox.foreach(self._remove_option) for option in CSS_STYLE_OPTIONS: value = app.css_config.get_value( option.selector, option.attr, pre=True) if value is None: continue row = Option(option, value) self._option_listbox.add(row) def _add_option(self, listbox, row): for option in self._option_listbox.get_children(): if option == row: return row = Option(row.option, None) self._option_listbox.add(row) def _clear_options(self): self._option_listbox.foreach(self._remove_option) def _fill_choose_listbox(self): for option in CSS_STYLE_OPTIONS: self._choose_option_listbox.add(ChooseOption(option)) def _remove_option(self, row): self._option_listbox.remove(row) row.destroy() def _on_add_new_theme(self, *args): name = self._create_theme_name() if not app.css_config.add_new_theme(name): return self._remove_theme_button.set_sensitive(True) iter_ = self._theme_store.append([name]) self._select_theme_row(iter_) @staticmethod def _create_theme_name(): i = 0 while 'newtheme%s' % i in app.css_config.themes: i += 1 return 'newtheme%s' % i def _on_remove_theme(self, *args): store, iter_ = self._theme_treeview.get_selection().get_selected() if iter_ is None: return theme = store[iter_][Column.THEME] if theme == app.config.get('roster_theme'): ErrorDialog( _('Active Theme'), _('You tried to delete the currently active theme. ' 'Please switch to a different theme first.'), transient_for=self) return def _remove_theme(): app.css_config.remove_theme(theme) store.remove(iter_) first = store.get_iter_first() if first is None: self._remove_theme_button.set_sensitive(False) self._add_option_button.set_sensitive(False) self._clear_options() buttons = { Gtk.ResponseType.CANCEL: DialogButton('Keep Theme'), Gtk.ResponseType.OK: DialogButton('Delete', _remove_theme, ButtonAction.DESTRUCTIVE), } NewConfirmationDialog('Delete Theme', 'Do you want to permanently delete this theme?', buttons, transient_for=self) @staticmethod def _on_destroy(*args): window = app.get_app_window('Preferences') if window is not None: window.update_theme_list() class Option(Gtk.ListBoxRow): def __init__(self, option, value): Gtk.ListBoxRow.__init__(self) self._option = option self._box = Gtk.Box(spacing=12) label = Gtk.Label() label.set_text(option.label) label.set_hexpand(True) label.set_halign(Gtk.Align.START) self._box.add(label) if option.attr in (StyleAttr.COLOR, StyleAttr.BACKGROUND): self._init_color(value) elif option.attr == StyleAttr.FONT: self._init_font(value) remove_button = Gtk.Button.new_from_icon_name( 'list-remove-symbolic', Gtk.IconSize.MENU) remove_button.get_style_context().add_class('theme_remove_button') remove_button.connect('clicked', self._on_remove) self._box.add(remove_button) self.add(self._box) self.show_all() def _init_color(self, color): color_button = Gtk.ColorButton() if color is not None: rgba = Gdk.RGBA() rgba.parse(color) color_button.set_rgba(rgba) color_button.set_halign(Gtk.Align.END) color_button.connect('color-set', self._on_color_set) self._box.add(color_button) def _init_font(self, desc): font_button = Gtk.FontButton() if desc is not None: font_button.set_font_desc(desc) font_button.set_halign(Gtk.Align.END) font_button.connect('font-set', self._on_font_set) self._box.add(font_button) def _on_color_set(self, color_button): color = color_button.get_rgba() color_string = color.to_string() app.css_config.set_value( self._option.selector, self._option.attr, color_string, pre=True) app.nec.push_incoming_event(StyleChanged(None)) def _on_font_set(self, font_button): desc = font_button.get_font_desc() app.css_config.set_font(self._option.selector, desc, pre=True) app.nec.push_incoming_event(StyleChanged(None,)) def _on_remove(self, *args): self.get_parent().remove(self) app.css_config.remove_value( self._option.selector, self._option.attr, pre=True) app.nec.push_incoming_event(StyleChanged(None)) self.destroy() def __eq__(self, other): if isinstance(other, ChooseOption): return other.option == self._option return other._option == self._option class ChooseOption(Gtk.ListBoxRow): def __init__(self, option): Gtk.ListBoxRow.__init__(self) self.option = option label = Gtk.Label(label=option.label) label.set_xalign(0) self.add(label) self.show_all()