2018-03-01 22:47:01 +01:00
|
|
|
# Copyright (C) 2018 Philipp Hörist <philipp AT hoerist.com>
|
|
|
|
#
|
|
|
|
# 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 <http://www.gnu.org/licenses/>.
|
|
|
|
|
|
|
|
from collections import namedtuple
|
|
|
|
from enum import IntEnum
|
|
|
|
|
|
|
|
from gi.repository import Gtk
|
|
|
|
from gi.repository import Gdk
|
|
|
|
|
|
|
|
from gajim.common import app
|
2019-01-03 08:33:35 +01:00
|
|
|
from gajim.common.nec import NetworkEvent
|
2018-09-13 23:56:12 +02:00
|
|
|
from gajim.common.i18n import _
|
2018-10-04 23:55:35 +02:00
|
|
|
from gajim.common.const import StyleAttr
|
|
|
|
from gajim.common.const import DialogButton
|
|
|
|
from gajim.common.const import ButtonAction
|
|
|
|
|
2018-09-26 19:06:47 +02:00
|
|
|
from gajim.gtk.dialogs import ErrorDialog
|
|
|
|
from gajim.gtk.dialogs import NewConfirmationDialog
|
2018-03-01 22:47:01 +01:00
|
|
|
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),
|
|
|
|
|
2018-08-19 12:15:07 +02:00
|
|
|
StyleOption(_('Account Row Font'),
|
2018-03-01 22:47:01 +01:00
|
|
|
'.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),
|
|
|
|
|
2018-08-19 12:15:07 +02:00
|
|
|
StyleOption(_('Group Row Font'),
|
2018-03-01 22:47:01 +01:00
|
|
|
'.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),
|
|
|
|
|
2018-08-19 12:15:07 +02:00
|
|
|
StyleOption(_('Contact Row Font'),
|
2018-03-01 22:47:01 +01:00
|
|
|
'.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 <b>default</b> 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)
|
2019-01-03 08:33:35 +01:00
|
|
|
app.nec.push_incoming_event(NetworkEvent('style-changed'))
|
2018-03-01 22:47:01 +01:00
|
|
|
|
|
|
|
def _on_font_set(self, font_button):
|
|
|
|
desc = font_button.get_font_desc()
|
|
|
|
app.css_config.set_font(self._option.selector, desc, pre=True)
|
2019-01-03 08:33:35 +01:00
|
|
|
app.nec.push_incoming_event(NetworkEvent('style-changed'))
|
2018-03-01 22:47:01 +01:00
|
|
|
|
|
|
|
def _on_remove(self, *args):
|
|
|
|
self.get_parent().remove(self)
|
|
|
|
app.css_config.remove_value(
|
|
|
|
self._option.selector, self._option.attr, pre=True)
|
2019-01-03 08:33:35 +01:00
|
|
|
app.nec.push_incoming_event(NetworkEvent('style-changed'))
|
2018-03-01 22:47:01 +01:00
|
|
|
self.destroy()
|
|
|
|
|
|
|
|
def __eq__(self, other):
|
|
|
|
if isinstance(other, ChooseOption):
|
|
|
|
return other.option == self._option
|
2018-09-18 10:14:04 +02:00
|
|
|
return other._option == self._option
|
2018-03-01 22:47:01 +01:00
|
|
|
|
|
|
|
|
|
|
|
class ChooseOption(Gtk.ListBoxRow):
|
|
|
|
def __init__(self, option):
|
|
|
|
Gtk.ListBoxRow.__init__(self)
|
|
|
|
self.option = option
|
2018-08-18 00:13:51 +02:00
|
|
|
label = Gtk.Label(label=option.label)
|
2018-03-01 22:47:01 +01:00
|
|
|
label.set_xalign(0)
|
|
|
|
self.add(label)
|
|
|
|
self.show_all()
|