diff --git a/gajim/application.py b/gajim/application.py
index e076abbc5..48789b0fc 100644
--- a/gajim/application.py
+++ b/gajim/application.py
@@ -160,9 +160,7 @@ class GajimApplication(Gtk.Application):
self.interface.roster.window.present()
return
from gajim.gui_interface import Interface
- from gajim import gtkgui_helpers
self.interface = Interface()
- gtkgui_helpers.load_css()
self.interface.run(self)
self.add_actions()
from gajim import gui_menu_builder
diff --git a/gajim/chat_control.py b/gajim/chat_control.py
index 82dda0baa..4c87ee452 100644
--- a/gajim/chat_control.py
+++ b/gajim/chat_control.py
@@ -674,7 +674,6 @@ class ChatControl(ChatControlBase):
status_reduced = ''
status_escaped = GLib.markup_escape_text(status_reduced)
- font_attrs, font_attrs_small = self.get_font_attrs()
st = app.config.get('displayed_chat_state_notifications')
cs = contact.chatstate
if cs and st in ('composing_only', 'all'):
@@ -685,22 +684,21 @@ class ChatControl(ChatControlBase):
else:
chatstate = ''
- label_text = '%s%s %s' \
- % (font_attrs, name, font_attrs_small, acct_info, chatstate)
+ label_text = '%s%s %s' \
+ % (name, acct_info, chatstate)
if acct_info:
acct_info = i18n.direction_mark + ' ' + acct_info
label_tooltip = '%s%s %s' % (name, acct_info, chatstate)
else:
- # weight="heavy" size="x-large"
- label_text = '%s%s' % \
- (font_attrs, name, font_attrs_small, acct_info)
+ label_text = '%s%s' % \
+ (name, acct_info)
if acct_info:
acct_info = i18n.direction_mark + ' ' + acct_info
label_tooltip = '%s%s' % (name, acct_info)
if status_escaped:
status_text = self.urlfinder.sub(self.make_href, status_escaped)
- status_text = '%s' % (font_attrs_small, status_text)
+ status_text = '%s' % status_text
self.banner_status_label.set_tooltip_text(status)
self.banner_status_label.set_no_show_all(False)
self.banner_status_label.show()
@@ -881,7 +879,7 @@ class ChatControl(ChatControlBase):
if self.correcting:
self.correcting = False
gtkgui_helpers.remove_css_class(
- self.msg_textview, 'msgcorrectingcolor')
+ self.msg_textview, 'gajim-msg-correcting')
self.print_conversation(obj.message, self.contact.jid, tim=obj.timestamp,
encrypted=obj.encrypted, xep0184_id=xep0184_id, xhtml=obj.xhtml,
diff --git a/gajim/chat_control_base.py b/gajim/chat_control_base.py
index 8e77b0dd4..7c8f9c347 100644
--- a/gajim/chat_control_base.py
+++ b/gajim/chat_control_base.py
@@ -38,6 +38,7 @@ from gi.repository import Gio
from gajim import gtkgui_helpers
from gajim import message_control
from gajim.gtk import NonModalConfirmationDialog
+from gajim.gtk.util import convert_rgb_to_hex
from gajim import notify
import re
@@ -52,6 +53,7 @@ from gajim.conversation_textview import ConversationTextview
from gajim.message_textview import MessageTextView
from gajim.common.contacts import GC_Contact
from gajim.common.connection_handlers_events import MessageOutgoingEvent
+from gajim.common.const import StyleAttr
from gajim.command_system.implementation.middleware import ChatCommandProcessor
from gajim.command_system.implementation.middleware import CommandTools
@@ -87,40 +89,13 @@ class ChatControlBase(MessageControl, ChatCommandProcessor, CommandTools):
keycode_ins = None
def make_href(self, match):
- url_color = app.config.get('urlmsgcolor')
+ url_color = app.css_config.get_value('.gajim-url', StyleAttr.COLOR)
+ color = convert_rgb_to_hex(url_color)
url = match.group()
- if not '://' in url:
+ if '://' not in url:
url = 'http://' + url
- return '%s' % (url,
- url_color, match.group())
-
- def get_font_attrs(self):
- """
- Get pango font attributes for banner from theme settings
- """
- theme = app.config.get('roster_theme')
- bannerfont = app.config.get_per('themes', theme, 'bannerfont')
- bannerfontattrs = app.config.get_per('themes', theme, 'bannerfontattrs')
-
- if bannerfont:
- font = Pango.FontDescription(bannerfont)
- else:
- font = Pango.FontDescription('Normal')
- if bannerfontattrs:
- # B attribute is set by default
- if 'B' in bannerfontattrs:
- font.set_weight(Pango.Weight.HEAVY)
- if 'I' in bannerfontattrs:
- font.set_style(Pango.Style.ITALIC)
-
- font_attrs = 'font_desc="%s"' % font.to_string()
-
- # in case there is no font specified we use x-large font size
- if font.get_size() == 0:
- font_attrs = '%s size="x-large"' % font_attrs
- font.set_weight(Pango.Weight.NORMAL)
- font_attrs_small = 'font_desc="%s" size="small"' % font.to_string()
- return (font_attrs, font_attrs_small)
+ return '%s' % (
+ url, color, match.group())
def get_nb_unread(self):
jid = self.contact.jid
@@ -381,6 +356,8 @@ class ChatControlBase(MessageControl, ChatCommandProcessor, CommandTools):
self._nec_ping)
app.ged.register_event_handler('sec-label-received', ged.GUI1,
self._sec_labels_received)
+ app.ged.register_event_handler('style-changed', ged.GUI1,
+ self._style_changed)
# This is basically a very nasty hack to surpass the inability
# to properly use the super, because of the old code.
@@ -557,6 +534,9 @@ class ChatControlBase(MessageControl, ChatCommandProcessor, CommandTools):
self._nec_our_status)
app.ged.remove_event_handler('sec-label-received', ged.GUI1,
self._sec_labels_received)
+ app.ged.remove_event_handler('style-changed', ged.GUI1,
+ self._style_changed)
+
def on_msg_textview_populate_popup(self, textview, menu):
"""
@@ -1099,6 +1079,9 @@ class ChatControlBase(MessageControl, ChatCommandProcessor, CommandTools):
def on_clear_formatting_menuitem_activate(self, widget):
self.msg_textview.clear_tags()
+ def _style_changed(self, *args):
+ self.update_tags()
+
def update_tags(self):
self.conv_textview.update_tags()
@@ -1365,14 +1348,14 @@ class ChatControlBase(MessageControl, ChatCommandProcessor, CommandTools):
history[pos - 1].startswith('/') or history[pos - 1].startswith('/me')):
self.correcting = True
gtkgui_helpers.add_css_class(
- self.msg_textview, 'msgcorrectingcolor')
+ self.msg_textview, 'gajim-msg-correcting')
message = history[pos - 1]
msg_buf.set_text(message)
return
if self.correcting:
# We were previously correcting
gtkgui_helpers.remove_css_class(
- self.msg_textview, 'msgcorrectingcolor')
+ self.msg_textview, 'gajim-msg-correcting')
self.correcting = False
pos += -1 if direction == 'up' else +1
if pos == -1:
diff --git a/gajim/common/app.py b/gajim/common/app.py
index 3e2a73697..0df58aa70 100644
--- a/gajim/common/app.py
+++ b/gajim/common/app.py
@@ -43,6 +43,7 @@ from gajim.common import configpaths
from gajim.common import ged as ged_module
from gajim.common.contacts import LegacyContactsAPI
from gajim.common.events import Events
+from gajim.common.css_config import CSSConfig
interface = None # The actual interface (the gtk one for the moment)
thread_interface = lambda *args: None # Interface to run a thread and then a callback
@@ -66,6 +67,8 @@ gajimpaths = configpaths.gajimpaths
RecentGroupchat = namedtuple('RecentGroupchat', ['room', 'server', 'nickname'])
+css_config = None
+
os_info = None # used to cache os information
transport_type = {} # list the type of transport
@@ -625,3 +628,7 @@ def get_app_window(cls, account=None):
continue
return win
return None
+
+def load_css_config():
+ global css_config
+ css_config = CSSConfig()
diff --git a/gajim/common/config.py b/gajim/common/config.py
index 8c9be2920..3fb649e1d 100644
--- a/gajim/common/config.py
+++ b/gajim/common/config.py
@@ -92,13 +92,6 @@ class Config:
'mood_iconset': [ opt_str, DEFAULT_MOOD_ICONSET, '', True ],
'activity_iconset': [ opt_str, DEFAULT_ACTIVITY_ICONSET, '', True ],
'use_transports_iconsets': [ opt_bool, True, '', True ],
- 'inmsgcolor': [ opt_color, '#a40000', _('Incoming nickname color.'), True ],
- 'outmsgcolor': [ opt_color, '#3465a4', _('Outgoing nickname color.'), True ],
- 'inmsgtxtcolor': [ opt_color, '', _('Incoming text color.'), True ],
- 'outmsgtxtcolor': [ opt_color, '#555753', _('Outgoing text color.'), True ],
- 'statusmsgcolor': [ opt_color, '#4e9a06', _('Status message text color.'), True ],
- 'markedmsgcolor': [ opt_color, '#ff8080', '', True ],
- 'urlmsgcolor': [ opt_color, '#204a87', '', True ],
'notif_signin_color': [ opt_color, '#32CD32', _('Contact signed in notification color.') ], # limegreen
'notif_signout_color': [ opt_color, '#FF0000', _('Contact signout notification color') ], # red
'notif_message_color': [ opt_color, '#1E90FF', _('New message notification color.') ], # dodgerblue
@@ -108,11 +101,6 @@ class Config:
'notif_invite_color': [ opt_color, '#D2B48C', _('Groupchat invitation notification color') ], # tan1
'notif_status_color': [ opt_color, '#D8BFD8', _('Background color of status changed notification') ], # thistle2
'notif_other_color': [ opt_color, '#FFFFFF', _('Other dialogs color.') ], # white
- 'inmsgfont': [ opt_str, '', _('Incoming nickname font.'), True ],
- 'outmsgfont': [ opt_str, '', _('Outgoing nickname font.'), True ],
- 'inmsgtxtfont': [ opt_str, '', _('Incoming text font.'), True ],
- 'outmsgtxtfont': [ opt_str, '', _('Outgoing text font.'), True ],
- 'statusmsgfont': [ opt_str, '', _('Status message text font.'), True ],
'collapsed_rows': [ opt_str, '', _('List (space separated) of rows (accounts and groups) that are collapsed.'), True ],
'roster_theme': [ opt_str, _('default'), '', True ],
'mergeaccounts': [ opt_bool, False, '', True ],
@@ -205,7 +193,6 @@ class Config:
'notify_on_file_complete': [opt_bool, True],
'file_transfers_port': [opt_int, 28011],
'ft_add_hosts_to_send': [opt_str, '', _('Comma separated list of sent hosts, in addition of local interfaces, for File Transfer in case of address translation/port forwarding.')],
- 'conversation_font': [opt_str, ''],
'use_kib_mib': [opt_bool, False, _('IEC standard says KiB = 1024 bytes, KB = 1000 bytes.')],
'notify_on_all_muc_messages': [opt_bool, False],
'trayicon_notification_on_events': [opt_bool, True, _('Notify of events in the notification area.')],
@@ -249,9 +236,6 @@ class Config:
'print_status_in_muc': [opt_str, 'none', _('Can be "none", "all" or "in_and_out". If "none", Gajim will no longer print status line in groupchats when a member changes their status and/or their status message. If "all" Gajim will print all status messages. If "in_and_out", Gajim will only print FOO enters/leaves group chat.')],
'log_contact_status_changes': [opt_bool, False],
'log_xhtml_messages': [opt_bool, False, _('Log XHTML messages instead of plain text messages.')],
- 'just_connected_bg_color': [opt_str, '#adc3c6', _('Background color of contacts when they just signed in.')],
- 'just_disconnected_bg_color': [opt_str, '#ab6161', _('Background color of contacts when they just signed out.')],
- 'restored_messages_color': [opt_color, '#555753'],
'restored_messages_small': [opt_bool, True, _('If true, restored messages will use a smaller font than the default one.')],
'hide_avatar_of_transport': [opt_bool, False, _('Don\'t show avatar for the transport itself.')],
'roster_window_skip_taskbar': [opt_bool, False, _('Don\'t show roster in the system taskbar.')],
@@ -438,35 +422,7 @@ class Config:
'bosh_http_pipelining': [ opt_bool, False ],
'bosh_wait_for_restart_response': [ opt_bool, False ],
}, {}),
- 'themes': ({
- 'accounttextcolor': [ opt_color, 'black', '', True ],
- 'accountbgcolor': [ opt_color, 'white', '', True ],
- 'accountfont': [ opt_str, '', '', True ],
- 'accountfontattrs': [ opt_str, 'B', '', True ],
- 'grouptextcolor': [ opt_color, 'black', '', True ],
- 'groupbgcolor': [ opt_color, 'white', '', True ],
- 'groupfont': [ opt_str, '', '', True ],
- 'groupfontattrs': [ opt_str, 'I', '', True ],
- 'contacttextcolor': [ opt_color, 'black', '', True ],
- 'contactbgcolor': [ opt_color, 'white', '', True ],
- 'contactfont': [ opt_str, '', '', True ],
- 'contactfontattrs': [ opt_str, '', '', True ],
- 'bannertextcolor': [ opt_color, 'black', '', True ],
- 'bannerbgcolor': [ opt_color, '', '', True ],
- 'bannerfont': [ opt_str, '', '', True ],
- 'bannerfontattrs': [ opt_str, 'B', '', True ],
- 'msgcorrectingcolor': [opt_color, '#eee8aa'],
- # http://www.pitt.edu/~nisg/cis/web/cgi/rgb.html
- 'state_inactive_color': [ opt_color, 'grey62' ],
- 'state_composing_color': [ opt_color, 'green4' ],
- 'state_paused_color': [ opt_color, 'mediumblue' ],
- 'state_gone_color': [ opt_color, 'grey' ],
-
- # MUC chat states
- 'state_muc_msg_color': [ opt_color, 'mediumblue' ],
- 'state_muc_directed_msg_color': [ opt_color, 'red2' ],
- }, {}),
'contacts': ({
'speller_language': [ opt_str, '', _('Language for which misspelled words will be checked')],
}, {}),
@@ -522,29 +478,6 @@ class Config:
'muc_message_received': [ False, 'gc_message2.wav', _('Sound to play when any MUC message arrives.') ],
}
- themes_default = {
- # sorted alphanum
- _('default'): [ '', '', '', 'B', '', '', '', 'I', '', '', '', '', '', '',
- '', 'B' ],
-
- _('green'): [ '', '#94aa8c', '', 'B', '#0000ff', '#eff3e7',
- '', 'I', '#000000', '', '', '', '',
- '#94aa8c', '', 'B' ],
-
- _('grocery'): [ '', '#6bbe18', '', 'B', '#12125a', '#ceefad',
- '', 'I', '#000000', '#efb26b', '', '', '',
- '#108abd', '', 'B' ],
-
- _('human'): [ '', '#996442', '', 'B', '#ab5920', '#e3ca94',
- '', 'I', '#000000', '', '', '', '',
- '#996442', '', 'B' ],
-
- _('marine'): [ '', '#918caa', '', 'B', '', '#e9e7f3',
- '', 'I', '#000000', '', '', '', '',
- '#918caa', '', 'B' ],
-
- }
-
proxies_default = {
_('Tor'): ['socks5', 'localhost', 9050],
}
diff --git a/gajim/common/configpaths.py b/gajim/common/configpaths.py
index bde9f63d5..cb2873943 100644
--- a/gajim/common/configpaths.py
+++ b/gajim/common/configpaths.py
@@ -128,6 +128,7 @@ class ConfigPaths:
source_paths = [
('DATA', os.path.join(basedir, 'data')),
+ ('STYLE', os.path.join(basedir, 'data', 'style')),
('GUI', os.path.join(basedir, 'data', 'gui')),
('ICONS', os.path.join(basedir, 'data', 'icons')),
('HOME', os.path.expanduser('~')),
@@ -214,6 +215,10 @@ class ConfigPaths:
# Cache paths
('CACHE_DB', 'cache.db', PathLocation.CACHE, PathType.FILE),
('AVATAR', 'avatars', PathLocation.CACHE, PathType.FOLDER),
+
+ # Config paths
+ ('MY_THEME', 'theme', PathLocation.CONFIG, PathType.FOLDER),
+
]
for path in paths:
diff --git a/gajim/common/connection_handlers_events.py b/gajim/common/connection_handlers_events.py
index 5b30a6156..038540e1c 100644
--- a/gajim/common/connection_handlers_events.py
+++ b/gajim/common/connection_handlers_events.py
@@ -1378,3 +1378,11 @@ class InformationEvent(nec.NetworkIncomingEvent):
else:
self.args = (self.args,)
return True
+
+
+class StyleChanged(nec.NetworkIncomingEvent):
+ name = 'style-changed'
+ base_network_events = []
+
+ def generate(self):
+ return True
diff --git a/gajim/common/const.py b/gajim/common/const.py
index ddca6865b..905743bec 100644
--- a/gajim/common/const.py
+++ b/gajim/common/const.py
@@ -1,9 +1,12 @@
-from enum import IntEnum, unique
+from enum import IntEnum, Enum, unique
from collections import namedtuple
Option = namedtuple('Option', 'kind label type value name callback data desc enabledif props')
Option.__new__.__defaults__ = (None,) * len(Option._fields)
+DialogButton = namedtuple('DialogButton', 'text callback action')
+DialogButton.__new__.__defaults__ = (None, None)
+
@unique
class OptionKind(IntEnum):
@@ -117,6 +120,24 @@ class JIDConstant(IntEnum):
NORMAL_TYPE = 0
ROOM_TYPE = 1
+@unique
+class StyleAttr(Enum):
+ COLOR = 'color'
+ BACKGROUND = 'background'
+ FONT = 'font'
+
+@unique
+class CSSPriority(IntEnum):
+ APPLICATION = 600
+ APPLICATION_DARK = 601
+ DEFAULT_THEME = 610
+ DEFAULT_THEME_DARK = 611
+ USER_THEME = 650
+
+@unique
+class ButtonAction(Enum):
+ DESTRUCTIVE = 'destructive-action'
+ SUGGESTED = 'suggested-action'
@unique
class IdleState(IntEnum):
diff --git a/gajim/common/css_config.py b/gajim/common/css_config.py
new file mode 100644
index 000000000..940ccc17e
--- /dev/null
+++ b/gajim/common/css_config.py
@@ -0,0 +1,492 @@
+# 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, either version 3 of the License, or
+# (at your option) any later version.
+#
+# 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 .
+
+import os
+import math
+import logging
+
+import cssutils
+from gi.repository import Gtk
+from gi.repository import Gdk
+from gi.repository import Pango
+
+from gajim.common import app
+from gajim.common import configpaths
+from gajim.common.const import StyleAttr, CSSPriority
+
+log = logging.getLogger('gajim.c.css')
+
+_settings = Gtk.Settings.get_default()
+PREFER_DARK = False
+if _settings is not None:
+ PREFER_DARK = _settings.get_property('gtk-application-prefer-dark-theme')
+
+
+class CSSConfig():
+ def __init__(self):
+ """CSSConfig handles loading and storing of all relevant Gajim style files
+
+ The order in which CSSConfig loads the styles
+
+ 1. gajim.css
+ 2. gajim-dark.css (Only if gtk-application-prefer-dark-theme = True)
+ 3. default.css or default-dark.css (from gajim/data/style)
+ 4. user-theme.css (from ~/.config/Gajim/theme)
+
+ # gajim.css:
+
+ This is the main style and the application default
+
+ # gajim-dark.css
+
+ Has only entrys which we want to override in gajim.css
+
+ # default.css or default-dark.css
+
+ Has all the values that are changeable via UI (see themes.py).
+ Depending on `gtk-application-prefer-dark-theme` either default.css or
+ default-dark.css gets loaded
+
+ # user-theme.css
+
+ These are the themes the Themes Dialog stores. Because they are
+ loaded at last they overwrite everything else. Users should add custom
+ css here."""
+
+ # Delete empty rules
+ cssutils.ser.prefs.keepEmptyRules = False
+
+ # Holds the currently selected theme in the Theme Editor
+ self._pre_css = None
+ self._pre_css_path = None
+
+ # Holds the default theme, its used if values are not found
+ # in the selected theme
+ self._default_css = None
+ self._default_css_path = None
+
+ # Holds the currently selected theme
+ self._css = None
+ self._css_path = None
+
+ # User Theme CSS Provider
+ self._provider = Gtk.CssProvider()
+
+ # Cache of recently requested values
+ self._cache = {}
+
+ # Holds all currently available themes
+ self.themes = []
+
+ self._load_css()
+ self._gather_available_themes()
+ self._load_default()
+ self._load_selected()
+ self._activate_theme()
+ Gtk.StyleContext.add_provider_for_screen(
+ Gdk.Screen.get_default(),
+ self._provider,
+ CSSPriority.USER_THEME)
+
+ def _load_css(self):
+ self._load_css_from_file('gajim.css', CSSPriority.APPLICATION)
+ if PREFER_DARK:
+ self._load_css_from_file('gajim-dark.css',
+ CSSPriority.APPLICATION_DARK)
+
+ self._load_css_from_file('default.css', CSSPriority.DEFAULT_THEME)
+ if PREFER_DARK:
+ self._load_css_from_file('default-dark.css',
+ CSSPriority.DEFAULT_THEME_DARK)
+
+ def _load_css_from_file(self, filename, priority):
+ path = os.path.join(configpaths.get('STYLE'), filename)
+ try:
+ with open(path, "r") as f:
+ css = f.read()
+ except Exception as exc:
+ log.error('Error loading css: %s', exc)
+ return
+ self._activate_css(css, priority)
+
+ def _activate_css(self, css, priority):
+ try:
+ provider = Gtk.CssProvider()
+ provider.load_from_data(bytes(css.encode('utf-8')))
+ Gtk.StyleContext.add_provider_for_screen(Gdk.Screen.get_default(),
+ provider,
+ priority)
+ except Exception:
+ log.exception('Error loading application css')
+
+ @staticmethod
+ def _pango_to_css_weight(number):
+ # Pango allows for weight values between 100 and 1000
+ # CSS allows only full hundred numbers like 100, 200 ..
+ number = int(number)
+ if number < 100:
+ return 100
+ if number > 900:
+ return 900
+ return int(math.ceil(number / 100.0)) * 100
+
+ def _gather_available_themes(self):
+ files = os.listdir(configpaths.get('MY_THEME'))
+ self.themes = [file[:-4] for file in files if file.endswith('.css')]
+ if 'default' in self.themes:
+ # Ignore user created themes that are named 'default'
+ self.themes.remove('default')
+
+ @classmethod
+ def get_theme_path(cls, theme, user=True):
+ if theme == 'default' and PREFER_DARK:
+ theme = 'default-dark'
+
+ if user:
+ return os.path.join(configpaths.get('MY_THEME'), '%s.css' % theme)
+ return os.path.join(configpaths.get('STYLE'), '%s.css' % theme)
+
+ def _determine_theme_path(self):
+ # Gets the path of the currently active theme.
+ # If it does not exist, it falls back to the default theme
+ theme = app.config.get('roster_theme')
+ if theme == 'default':
+ return self.get_theme_path(theme, user=False)
+
+ theme_path = self.get_theme_path(theme)
+ if not theme or not os.path.exists(theme_path):
+ log.warning('Theme %s not found, fallback to default', theme)
+ app.config.set('roster_theme', 'default')
+ log.info('Use Theme: default')
+ return self.get_theme_path('default', user=False)
+ log.info('Use Theme: %s', theme)
+ return theme_path
+
+ def _load_selected(self, new_path=None):
+ if new_path is None:
+ self._css_path = self._determine_theme_path()
+ else:
+ self._css_path = new_path
+ self._css = cssutils.parseFile(self._css_path)
+
+ def _load_default(self):
+ self._default_css_path = self.get_theme_path('default', user=False)
+ self._default_css = cssutils.parseFile(self._default_css_path)
+
+ def _load_pre(self, theme):
+ log.info('Preload theme %s', theme)
+ self._pre_css_path = self.get_theme_path(theme)
+ self._pre_css = cssutils.parseFile(self._pre_css_path)
+
+ def _write(self, pre):
+ path = self._css_path
+ css = self._css
+ if pre:
+ path = self._pre_css_path
+ css = self._pre_css
+ with open(path, 'w', encoding='utf-8') as file:
+ file.write(css.cssText.decode('utf-8'))
+
+ active = self._pre_css_path == self._css_path
+ if not pre or active:
+ self._load_selected()
+ self._activate_theme()
+
+ def set_value(self, selector, attr, value, pre=False):
+ if attr == StyleAttr.FONT:
+ # forward to set_font() for convenience
+ return self.set_font(selector, value, pre)
+
+ if isinstance(attr, StyleAttr):
+ attr = attr.value
+
+ css = self._css
+ if pre:
+ css = self._pre_css
+ for rule in css:
+ if rule.type != rule.STYLE_RULE:
+ continue
+ if rule.selectorText == selector:
+ log.info('Set %s %s %s', selector, attr, value)
+ rule.style[attr] = value
+ if not pre:
+ self._add_to_cache(selector, attr, value)
+ self._write(pre)
+ return
+
+ # The rule was not found, so we add it to this theme
+ log.info('Set %s %s %s', selector, attr, value)
+ rule = cssutils.css.CSSStyleRule(selectorText=selector)
+ rule.style[attr] = value
+ css.add(rule)
+ self._write(pre)
+
+ def set_font(self, selector, description, pre=False):
+ css = self._css
+ if pre:
+ css = self._pre_css
+ family, size, style, weight = self._get_attr_from_description(
+ description)
+ for rule in css:
+ if rule.type != rule.STYLE_RULE:
+ continue
+ if rule.selectorText == selector:
+ log.info('Set Font for: %s %s %s %s %s',
+ selector, family, size, style, weight)
+ rule.style['font-family'] = family
+ rule.style['font-style'] = style
+ rule.style['font-size'] = '%spt' % size
+ rule.style['font-weight'] = weight
+
+ if not pre:
+ self._add_to_cache(
+ selector, 'fontdescription', description)
+ self._write(pre)
+ return
+
+ # The rule was not found, so we add it to this theme
+ log.info('Set Font for: %s %s %s %s %s',
+ selector, family, size, style, weight)
+ rule = cssutils.css.CSSStyleRule(selectorText=selector)
+ rule.style['font-family'] = family
+ rule.style['font-style'] = style
+ rule.style['font-size'] = '%spt' % size
+ rule.style['font-weight'] = weight
+ css.add(rule)
+ self._write(pre)
+
+ def _get_attr_from_description(self, description):
+ size = description.get_size() / Pango.SCALE
+ style = self._get_string_from_pango_style(description.get_style())
+ weight = self._pango_to_css_weight(int(description.get_weight()))
+ family = description.get_family()
+ return family, size, style, weight
+
+ def _get_default_rule(self, selector, attr):
+ for rule in self._default_css:
+ if rule.type != rule.STYLE_RULE:
+ continue
+ if rule.selectorText == selector:
+ log.info('Get Default Rule %s', selector)
+ return rule
+
+ def get_font(self, selector, pre=False):
+ if pre:
+ css = self._pre_css
+ else:
+ css = self._css
+ try:
+ return self._get_from_cache(selector, 'fontdescription')
+ except KeyError:
+ pass
+
+ if css is None:
+ return
+
+ for rule in css:
+ if rule.type != rule.STYLE_RULE:
+ continue
+ if rule.selectorText == selector:
+ log.info('Get Font for: %s', selector)
+ style = rule.style.getPropertyValue('font-style') or None
+ size = rule.style.getPropertyValue('font-size') or None
+ weight = rule.style.getPropertyValue('font-weight') or None
+ family = rule.style.getPropertyValue('font-family') or None
+
+ desc = self._get_description_from_css(
+ family, size, style, weight)
+ if not pre:
+ self._add_to_cache(selector, 'fontdescription', desc)
+ return desc
+
+ def _get_description_from_css(self, family, size, style, weight):
+ if family is None:
+ return
+ desc = Pango.FontDescription()
+ desc.set_family(family)
+ if weight is not None:
+ desc.set_weight(Pango.Weight(int(weight)))
+ if style is not None:
+ desc.set_style(self._get_pango_style_from_string(style))
+ if size is not None:
+ desc.set_size(int(size[:-2]) * Pango.SCALE)
+ return desc
+
+ @staticmethod
+ def _get_pango_style_from_string(style: str) -> int:
+ if style == 'normal':
+ return Pango.Style(0)
+ if style == 'oblique':
+ return Pango.Style(1)
+ # Pango.Style.ITALIC:
+ return Pango.Style(2)
+
+ @staticmethod
+ def _get_string_from_pango_style(style: Pango.Style) -> str:
+ if style == Pango.Style.NORMAL:
+ return 'normal'
+ if style == Pango.Style.ITALIC:
+ return 'italic'
+ # Pango.Style.OBLIQUE:
+ return 'oblique'
+
+ def get_value(self, selector, attr, pre=False):
+ if attr == StyleAttr.FONT:
+ # forward to get_font() for convenience
+ return self.get_font(selector, pre)
+
+ if isinstance(attr, StyleAttr):
+ attr = attr.value
+
+ if pre:
+ css = self._pre_css
+ else:
+ css = self._css
+ try:
+ return self._get_from_cache(selector, attr)
+ except KeyError:
+ pass
+
+ if css is not None:
+ for rule in css:
+ if rule.type != rule.STYLE_RULE:
+ continue
+ if rule.selectorText == selector:
+ log.info('Get %s %s: %s',
+ selector, attr, rule.style[attr] or None)
+ value = rule.style.getPropertyValue(attr) or None
+ if not pre:
+ self._add_to_cache(selector, attr, value)
+ return value
+
+ # We didnt find the selector in the selected theme
+ # search in default theme
+ if not pre:
+ rule = self._get_default_rule(selector, attr)
+ if rule is not None:
+ self._add_to_cache(selector, attr, rule.style[attr])
+ return rule.style[attr]
+
+ def remove_value(self, selector, attr, pre=False):
+ if attr == StyleAttr.FONT:
+ # forward to remove_font() for convenience
+ return self.remove_font(selector, pre)
+
+ if isinstance(attr, StyleAttr):
+ attr = attr.value
+
+ css = self._css
+ if pre:
+ css = self._pre_css
+ for rule in css:
+ if rule.type != rule.STYLE_RULE:
+ continue
+ if rule.selectorText == selector:
+ log.info('Remove %s %s', selector, attr)
+ rule.style.removeProperty(attr)
+ break
+ self._write(pre)
+
+ def remove_font(self, selector, pre=False):
+ css = self._css
+ if pre:
+ css = self._pre_css
+
+ for rule in css:
+ if rule.type != rule.STYLE_RULE:
+ continue
+ if rule.selectorText == selector:
+ log.info('Remove Font from: %s', selector)
+ rule.style.removeProperty('font-family')
+ rule.style.removeProperty('font-size')
+ rule.style.removeProperty('font-style')
+ rule.style.removeProperty('font-weight')
+ break
+ self._write(pre)
+
+ def change_theme(self, theme):
+ user = not theme == 'default'
+ theme_path = self.get_theme_path(theme, user=user)
+ if not os.path.exists(theme_path):
+ log.error('Change Theme: Theme %s does not exist', theme_path)
+ return False
+ self._load_selected(theme_path)
+ self._activate_theme()
+ app.config.set('roster_theme', theme)
+ log.info('Change Theme: Successful switched to %s', theme)
+ return True
+
+ def change_preload_theme(self, theme):
+ theme_path = self.get_theme_path(theme)
+ if not os.path.exists(theme_path):
+ log.error('Change Preload Theme: Theme %s does not exist',
+ theme_path)
+ return False
+ self._load_pre(theme)
+ log.info('Successful switched to %s', theme)
+ return True
+
+ def rename_theme(self, old_theme, new_theme):
+ if old_theme not in self.themes:
+ log.error('Rename Theme: Old theme %s not found', old_theme)
+ return False
+
+ if new_theme in self.themes:
+ log.error('Rename Theme: New theme %s exists already', new_theme)
+ return False
+
+ old_theme_path = self.get_theme_path(old_theme)
+ new_theme_path = self.get_theme_path(new_theme)
+ os.rename(old_theme_path, new_theme_path)
+ self.themes.remove(old_theme)
+ self.themes.append(new_theme)
+ self._load_pre(new_theme)
+ log.info('Rename Theme: Successful renamed theme from %s to %s',
+ old_theme, new_theme)
+ return True
+
+ def _activate_theme(self):
+ log.info('Activate theme')
+ self._invalidate_cache()
+ self._provider.load_from_data(self._css.cssText)
+
+ def add_new_theme(self, theme):
+ theme_path = self.get_theme_path(theme)
+ if os.path.exists(theme_path):
+ log.error('Add Theme: %s exists already', theme_path)
+ return False
+ with open(theme_path, 'w', encoding='utf8'):
+ pass
+ self.themes.append(theme)
+ log.info('Add Theme: Successful added theme %s', theme)
+ return True
+
+ def remove_theme(self, theme):
+ theme_path = self.get_theme_path(theme)
+ if os.path.exists(theme_path):
+ os.remove(theme_path)
+ self.themes.remove(theme)
+ log.info('Remove Theme: Successful removed theme %s', theme)
+
+ def _add_to_cache(self, selector, attr, value):
+ self._cache[selector + attr] = value
+
+ def _get_from_cache(self, selector, attr):
+ return self._cache[selector + attr]
+
+ def _invalidate_cache(self):
+ self._cache = {}
diff --git a/gajim/conversation_textview.py b/gajim/conversation_textview.py
index 7034c0062..0d3044c08 100644
--- a/gajim/conversation_textview.py
+++ b/gajim/conversation_textview.py
@@ -49,6 +49,7 @@ from gajim.common import i18n
from calendar import timegm
from gajim.common.fuzzyclock import FuzzyClock
from gajim import emoticons
+from gajim.common.const import StyleAttr
from gajim.htmltextview import HtmlTextView
@@ -216,42 +217,47 @@ class ConversationTextview(GObject.GObject):
self.last_time_printout = 0
style = self.tv.get_style_context()
- style.add_class('font_custom')
+ style.add_class('gajim-conversation-font')
buffer_ = self.tv.get_buffer()
end_iter = buffer_.get_end_iter()
buffer_.create_mark('end', end_iter, False)
self.tagIn = buffer_.create_tag('incoming')
- color = app.config.get('inmsgcolor')
- font = Pango.FontDescription(app.config.get('inmsgfont'))
+ color = app.css_config.get_value(
+ '.gajim-incoming-nickname', StyleAttr.COLOR)
self.tagIn.set_property('foreground', color)
- self.tagIn.set_property('font-desc', font)
+ desc = app.css_config.get_font('.gajim-incoming-nickname')
+ self.tagIn.set_property('font-desc', desc)
self.tagOut = buffer_.create_tag('outgoing')
- color = app.config.get('outmsgcolor')
- font = Pango.FontDescription(app.config.get('outmsgfont'))
+ color = app.css_config.get_value(
+ '.gajim-outgoing-nickname', StyleAttr.COLOR)
self.tagOut.set_property('foreground', color)
- self.tagOut.set_property('font-desc', font)
+ desc = app.css_config.get_font('.gajim-outgoing-nickname')
+ self.tagOut.set_property('font-desc', desc)
self.tagStatus = buffer_.create_tag('status')
- color = app.config.get('statusmsgcolor')
- font = Pango.FontDescription(app.config.get('satusmsgfont'))
+ color = app.css_config.get_value(
+ '.gajim-status-message', StyleAttr.COLOR)
self.tagStatus.set_property('foreground', color)
- self.tagStatus.set_property('font-desc', font)
+ desc = app.css_config.get_font('.gajim-status-message')
+ self.tagStatus.set_property('font-desc', desc)
self.tagInText = buffer_.create_tag('incomingtxt')
- color = app.config.get('inmsgtxtcolor')
- font = Pango.FontDescription(app.config.get('inmsgtxtfont'))
+ color = app.css_config.get_value(
+ '.gajim-incoming-message-text', StyleAttr.COLOR)
if color:
self.tagInText.set_property('foreground', color)
- self.tagInText.set_property('font-desc', font)
+ desc = app.css_config.get_font('.gajim-incoming-message-text')
+ self.tagInText.set_property('font-desc', desc)
self.tagOutText = buffer_.create_tag('outgoingtxt')
- color = app.config.get('outmsgtxtcolor')
+ color = app.css_config.get_value(
+ '.gajim-outgoing-message-text', StyleAttr.COLOR)
if color:
- font = Pango.FontDescription(app.config.get('outmsgtxtfont'))
- self.tagOutText.set_property('foreground', color)
- self.tagOutText.set_property('font-desc', font)
+ self.tagOutText.set_property('foreground', color)
+ desc = app.css_config.get_font('.gajim-outgoing-message-text')
+ self.tagOutText.set_property('font-desc', desc)
colors = app.config.get('gc_nicknames_colors')
colors = colors.split(':')
@@ -261,7 +267,8 @@ class ConversationTextview(GObject.GObject):
tag.set_property('foreground', color)
self.tagMarked = buffer_.create_tag('marked')
- color = app.config.get('markedmsgcolor')
+ color = app.css_config.get_value(
+ '.gajim-highlight-message', StyleAttr.COLOR)
self.tagMarked.set_property('foreground', color)
self.tagMarked.set_property('weight', Pango.Weight.BOLD)
@@ -276,7 +283,7 @@ class ConversationTextview(GObject.GObject):
tag.set_property('scale', 0.8333333333333)
tag = buffer_.create_tag('restored_message')
- color = app.config.get('restored_messages_color')
+ color = app.css_config.get_value('.gajim-restored-message', StyleAttr.COLOR)
tag.set_property('foreground', color)
self.tv.create_tags()
@@ -355,13 +362,13 @@ class ConversationTextview(GObject.GObject):
self.tv.destroy()
def update_tags(self):
- self.tagIn.set_property('foreground', app.config.get('inmsgcolor'))
- self.tagOut.set_property('foreground', app.config.get('outmsgcolor'))
+ self.tagIn.set_property('foreground', app.css_config.get_value('.gajim-incoming-nickname', StyleAttr.COLOR))
+ self.tagOut.set_property('foreground', app.css_config.get_value('.gajim-outgoing-nickname', StyleAttr.COLOR))
self.tagStatus.set_property('foreground',
- app.config.get('statusmsgcolor'))
+ app.css_config.get_value('.gajim-status-message', StyleAttr.COLOR))
self.tagMarked.set_property('foreground',
- app.config.get('markedmsgcolor'))
- color = app.config.get('urlmsgcolor')
+ app.css_config.get_value('.gajim-highlight-message', StyleAttr.COLOR))
+ color = app.css_config.get_value('.gajim-url', StyleAttr.COLOR)
self.tv.tagURL.set_property('foreground', color)
self.tv.tagMail.set_property('foreground', color)
self.tv.tagXMPP.set_property('foreground', color)
diff --git a/gajim/data/gui/chat_control.ui b/gajim/data/gui/chat_control.ui
index 8cb5dca03..d0309c7ef 100644
--- a/gajim/data/gui/chat_control.ui
+++ b/gajim/data/gui/chat_control.ui
@@ -567,6 +567,9 @@
+
False
diff --git a/gajim/data/gui/gajim_themes_window.ui b/gajim/data/gui/gajim_themes_window.ui
deleted file mode 100644
index e4492a883..000000000
--- a/gajim/data/gui/gajim_themes_window.ui
+++ /dev/null
@@ -1,565 +0,0 @@
-
-
-
-
-
-
-
diff --git a/gajim/data/gui/groupchat_control.ui b/gajim/data/gui/groupchat_control.ui
index a7af4e00c..f98bcb641 100644
--- a/gajim/data/gui/groupchat_control.ui
+++ b/gajim/data/gui/groupchat_control.ui
@@ -159,6 +159,9 @@
+
False
diff --git a/gajim/data/gui/preferences_window.ui b/gajim/data/gui/preferences_window.ui
index a64ed23fb..67814e62b 100644
--- a/gajim/data/gui/preferences_window.ui
+++ b/gajim/data/gui/preferences_window.ui
@@ -1509,86 +1509,6 @@ $T will be replaced by auto-not-available timeout
vertical
12
-
- True
- False
- 0
- none
-
-
- True
- False
- 12
- 6
- 6
- 12
-
-
- True
- False
- GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK
- end
- Chat message
- right
- start
-
-
-
- 0
- 0
-
-
-
-
- 200
- True
- True
- False
- Sans 12
-
-
-
- 1
- 0
-
-
-
-
- Use system _default
- True
- True
- False
- start
- True
- True
-
-
-
- 1
- 1
-
-
-
-
-
-
-
-
-
- True
- False
- <b>Font</b>
- True
-
-
-
-
- False
- True
- 0
-
@@ -1658,11 +1578,10 @@ $T will be replaced by auto-not-available timeout
-
+
200
True
False
-
1
@@ -1721,423 +1640,6 @@ $T will be replaced by auto-not-available timeout
-
- True
- False
- 0
- none
-
-
- True
- False
- start
- 12
- 6
- 6
- 12
-
-
- True
- False
- end
- Contact's nickname
- True
- center
-
-
-
- 0
- 0
-
-
-
-
- True
- False
- end
- Contact's message
- right
-
-
-
- 0
- 1
-
-
-
-
- True
- False
- end
- _Status message
- True
- center
-
-
-
- 0
- 2
-
-
-
-
- True
- False
- end
- Group chat highlight
- True
- right
-
-
-
- 0
- 3
-
-
-
-
- True
- False
- 6
-
-
- True
- True
- False
- start
- True
-
-
-
- False
- False
- 0
-
-
-
-
- True
- True
- True
- start
- rgb(0,0,0)
-
-
-
- False
- False
- end
- 1
-
-
-
-
- 1
- 1
-
-
-
-
- True
- False
-
-
- True
- True
- True
- start
- rgb(0,0,0)
-
-
-
- False
- True
- end
- 0
-
-
-
-
- 1
- 2
-
-
-
-
- True
- False
-
-
-
-
-
- True
- True
- True
- start
- rgb(0,0,0)
-
-
-
- False
- True
- end
- 1
-
-
-
-
- 1
- 0
-
-
-
-
- True
- False
-
-
-
-
-
- True
- True
- True
- start
- rgb(0,0,0)
-
-
-
- False
- True
- end
- 1
-
-
-
-
- 1
- 3
-
-
-
-
- True
- False
- end
- Your nickname
- True
- center
-
-
-
- 3
- 0
-
-
-
-
- True
- False
- end
- Your message
- right
-
-
-
- 3
- 1
-
-
-
-
- True
- False
- end
- _URL highlight
- True
- right
-
-
-
- 3
- 2
-
-
-
-
- True
- False
-
-
-
-
-
- True
- True
- True
- start
- rgb(0,0,0)
-
-
-
- False
- True
- end
- 1
-
-
-
-
- 4
- 0
-
-
-
-
- True
- False
- 6
-
-
- True
- True
- False
- start
- True
-
-
-
- False
- False
- 0
-
-
-
-
- True
- True
- True
- start
- rgb(0,0,0)
-
-
-
- False
- False
- end
- 1
-
-
-
-
- 4
- 1
-
-
-
-
- True
- False
-
-
-
-
-
- True
- True
- True
- start
- rgb(0,0,0)
-
-
-
- False
- True
- end
- 1
-
-
-
-
- 4
- 2
-
-
-
-
- 6
- True
- False
-
-
- 2
- 0
-
-
-
-
- _Reset to Default Colors
- True
- True
- False
- False
- end
- 6
- True
-
-
-
- 0
- 4
- 5
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- True
- False
- <b>Chat Line Colors</b>
- True
-
-
-
-
- False
- True
- 2
-
diff --git a/gajim/data/gui/service_discovery_window.ui b/gajim/data/gui/service_discovery_window.ui
index 16fe77569..29379aef3 100644
--- a/gajim/data/gui/service_discovery_window.ui
+++ b/gajim/data/gui/service_discovery_window.ui
@@ -65,6 +65,9 @@ Agent JID - node
+
False
diff --git a/gajim/data/gui/themes_window.ui b/gajim/data/gui/themes_window.ui
new file mode 100644
index 000000000..d000d845f
--- /dev/null
+++ b/gajim/data/gui/themes_window.ui
@@ -0,0 +1,208 @@
+
+
+
+
+
+ False
+
+
+ True
+ True
+ never
+ in
+ 200
+
+
+ True
+ False
+
+
+ True
+ False
+ none
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ True
+ False
+ 6
+ 6
+
+
+ True
+ True
+ never
+ in
+
+
+ 150
+ True
+ True
+ theme_store
+
+
+
+
+
+
+
+ Themes
+
+
+ True
+
+
+
+ 0
+
+
+
+
+
+
+
+
+ 0
+ 0
+
+
+
+
+ True
+ False
+
+
+ True
+ True
+ True
+
+
+
+ True
+ False
+ list-add-symbolic
+ 1
+
+
+
+
+ False
+ False
+ 0
+
+
+
+
+ True
+ False
+ True
+ True
+
+
+
+ True
+ False
+ list-remove-symbolic
+ 1
+
+
+
+
+ False
+ False
+ 1
+
+
+
+
+
+ 0
+ 1
+
+
+
+
+ True
+ True
+ True
+ never
+ in
+
+
+ True
+ False
+
+
+ 200
+ True
+ False
+ True
+ none
+ False
+
+
+
+
+
+
+
+ 1
+ 0
+
+
+
+
+ True
+ False
+
+
+
+ False
+ True
+ end
+ 0
+
+
+
+
+
+ 1
+ 1
+
+
+
+
diff --git a/gajim/data/style/default-dark.css b/gajim/data/style/default-dark.css
new file mode 100644
index 000000000..dabf5dc1c
--- /dev/null
+++ b/gajim/data/style/default-dark.css
@@ -0,0 +1,18 @@
+.gajim-incoming-nickname {
+ color: rgb(164, 0, 0)
+ }
+.gajim-outgoing-nickname {
+ color: rgb(52, 101, 164)
+ }
+.gajim-url {
+ color: rgb(117, 80, 123)
+ }
+.gajim-highlight-message {
+ color: rgb(245, 121, 0)
+ }
+.gajim-msg-correcting text {
+ background: rgb(131, 148, 150)
+ }
+.gajim-status-message {
+ color: rgb(115, 210, 22)
+ }
\ No newline at end of file
diff --git a/gajim/data/style/default.css b/gajim/data/style/default.css
new file mode 100644
index 000000000..5e34b8508
--- /dev/null
+++ b/gajim/data/style/default.css
@@ -0,0 +1 @@
+
.gajim-incoming-nickname {
color: rgb(164, 0, 0)
}
.gajim-outgoing-nickname {
color: rgb(52, 101, 164)
}
.gajim-outgoing-message-text {
color: rgb(85, 87, 83)
}
.gajim-status-message {
color: rgb(78, 154, 6)
}
.gajim-url {
color: rgb(32, 74, 135)
}
.gajim-highlight-message {
color: rgb(255, 128, 128)
}
.gajim-restored-message {
color: rgb(85, 87, 83)
}
.gajim-roster-disconnected {
background: rgb(171 ,97 ,97)
}
.gajim-roster-connected {
background: rgb(173, 195, 198)
}
\ No newline at end of file
diff --git a/gajim/data/style/gajim-dark.css b/gajim/data/style/gajim-dark.css
new file mode 100644
index 000000000..e6965eb52
--- /dev/null
+++ b/gajim/data/style/gajim-dark.css
@@ -0,0 +1,2 @@
+/* Gajim Dark Application CSS File */
+
diff --git a/gajim/data/style/gajim.css b/gajim/data/style/gajim.css
index 510d4f751..7e89ed7d0 100644
--- a/gajim/data/style/gajim.css
+++ b/gajim/data/style/gajim.css
@@ -1,5 +1,31 @@
/* Gajim Application CSS File */
+.gajim-state-composing {
+ color: rgb(0, 139, 0)
+ }
+.gajim-state-inactive {
+ color: rgb(158, 158, 158)
+ }
+.gajim-state-gone {
+ color: rgb(128, 128, 128)
+ }
+.gajim-state-paused {
+ color: rgb(0, 0, 205)
+ }
+.gajim-msg-correcting text {
+ background: rgb(238, 232, 170)
+ }
+.gajim-state-tab-muc-directed-msg {
+ color: rgb(238, 0, 0)
+ }
+.gajim-state-tab-muc-msg {
+ color: rgb(0, 0, 205)
+ }
+.gajim-banner {
+ font-size: 13pt;
+ font-weight: 700
+ }
+
.chatcontrol-actionbar-button {
padding: 0px 5px 0px 5px;
@@ -136,6 +162,28 @@ list.settings > row > box {
opacity: 0.95;
}
+/* ThemesWindow */
+#ThemesWindow grid { padding: 12px; }
+
+.theme_listbox row { border-bottom: 1px solid; border-color: @theme_unfocused_bg_color; padding: 5px 10px 5px 10px;}
+.theme_listbox row:last-child { border-bottom: 0px}
+.theme_listbox row.activatable:active { box-shadow: none; }
+.theme_listbox row:not(.activatable) label { color: @insensitive_fg_color }
+.theme_listbox row:focus { outline: none; }
+.theme_listbox row:hover { background-color: @theme_base_color }
+
+.theme_remove_button {
+ background-color: @theme_base_color;
+ border: none;
+ background-image: none; }
+
+.theme_remove_button:focus {outline: none;}
+
+.theme_popover_listbox { padding: 10px; }
+.theme_popover_listbox row { padding: 5px; }
+.theme_popover_listbox row:focus { outline: none; }
+
+
/* Text style */
.bold16 { font-size: 16px; font-weight: bold; }
diff --git a/gajim/dialogs.py b/gajim/dialogs.py
index 30c1f1a92..b3392b9ed 100644
--- a/gajim/dialogs.py
+++ b/gajim/dialogs.py
@@ -1058,7 +1058,6 @@ class SynchroniseSelectContactsDialog:
iter_ = model.iter_next(iter_)
self.dialog.destroy()
-
#Action that can be done with an incoming list of contacts
TRANSLATED_ACTION = {'add': _('add'), 'modify': _('modify'),
'remove': _('remove')}
diff --git a/gajim/disco.py b/gajim/disco.py
index 4fe38e86f..dc4d5284a 100644
--- a/gajim/disco.py
+++ b/gajim/disco.py
@@ -65,6 +65,7 @@ from gajim.common import app
import nbxmpp
from gajim.common import helpers
from gajim.common import ged
+from gajim.common.const import StyleAttr
LABELS = {
1: _('This service has not yet responded with detailed information'),
@@ -597,34 +598,10 @@ _('Without a connection, you can not browse available services'))
self.banner_icon.clear()
self.banner_icon.hide() # Just clearing it doesn't work
- def _set_window_banner_text(self, text, text_after = None):
- theme = app.config.get('roster_theme')
- bannerfont = app.config.get_per('themes', theme, 'bannerfont')
- bannerfontattrs = app.config.get_per('themes', theme,
- 'bannerfontattrs')
-
- if bannerfont:
- font = Pango.FontDescription(bannerfont)
- else:
- font = Pango.FontDescription('Normal')
- if bannerfontattrs:
- # B is attribute set by default
- if 'B' in bannerfontattrs:
- font.set_weight(Pango.Weight.HEAVY)
- if 'I' in bannerfontattrs:
- font.set_style(Pango.Style.ITALIC)
-
- font_attrs = 'font_desc="%s"' % font.to_string()
- font_size = font.get_size()
-
- # in case there is no font specified we use x-large font size
- if font_size == 0:
- font_attrs = '%s size="large"' % font_attrs
- markup = '%s' % (font_attrs, text)
- if text_after:
- font.set_weight(Pango.Weight.NORMAL)
- markup = '%s\n%s' % \
- (markup, font.to_string(), text_after)
+ def _set_window_banner_text(self, text, text_after=None):
+ markup = '%s' % (text)
+ if text_after is not None:
+ markup += '\n%s' % text_after
self.banner.set_markup(markup)
def destroy(self, chain = False):
@@ -1195,8 +1172,7 @@ class ToplevelAgentBrowser(AgentBrowser):
# Normal/success
cell.set_property('foreground_set', False)
else:
- theme = app.config.get('roster_theme')
- bgcolor = app.config.get_per('themes', theme, 'groupbgcolor')
+ bgcolor = app.css_config.get_value('.gajim-group-row', StyleAttr.BACKGROUND)
if bgcolor:
cell.set_property('cell_background_set', True)
cell.set_property('foreground_set', False)
@@ -1329,8 +1305,7 @@ class ToplevelAgentBrowser(AgentBrowser):
AgentBrowser.cleanup(self)
def update_theme(self):
- theme = app.config.get('roster_theme')
- bgcolor = app.config.get_per('themes', theme, 'groupbgcolor')
+ bgcolor = app.css_config.get_value('.gajim-group-row', StyleAttr.BACKGROUND)
if bgcolor:
self._renderer.set_property('cell-background', bgcolor)
self.window.services_treeview.queue_draw()
diff --git a/gajim/gajim_themes_window.py b/gajim/gajim_themes_window.py
deleted file mode 100644
index f39fbff43..000000000
--- a/gajim/gajim_themes_window.py
+++ /dev/null
@@ -1,411 +0,0 @@
-# -*- coding:utf-8 -*-
-## src/gajim_themes_window.py
-##
-## Copyright (C) 2003-2014 Yann Leboulanger
-## Copyright (C) 2005-2006 Dimitur Kirov
-## Nikos Kouremenos
-## Copyright (C) 2006 Jean-Marie Traissard
-## Copyright (C) 2007 Stephan Erb
-##
-## 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 gi.repository import Gtk
-from gi.repository import Gdk
-from gi.repository import Pango
-
-from gajim.common import app
-
-from gajim.gtk import ErrorDialog
-from gajim.gtk.util import get_builder
-
-
-class GajimThemesWindow:
-
- def __init__(self, transient):
- self.xml = get_builder('gajim_themes_window.ui')
- self.window = self.xml.get_object('gajim_themes_window')
- self.window.set_transient_for(transient)
-
- self.options = ['account', 'group', 'contact', 'banner']
- self.options_combobox = self.xml.get_object('options_combobox')
- self.textcolor_checkbutton = self.xml.get_object('textcolor_checkbutton')
- self.background_checkbutton = self.xml.get_object('background_checkbutton')
- self.textfont_checkbutton = self.xml.get_object('textfont_checkbutton')
- self.text_colorbutton = self.xml.get_object('text_colorbutton')
- self.background_colorbutton = self.xml.get_object('background_colorbutton')
- self.text_fontbutton = self.xml.get_object('text_fontbutton')
- self.bold_togglebutton = self.xml.get_object('bold_togglebutton')
- self.italic_togglebutton = self.xml.get_object('italic_togglebutton')
- self.themes_tree = self.xml.get_object('themes_treeview')
- self.theme_options_vbox = self.xml.get_object('theme_options_vbox')
- self.theme_options_table = self.xml.get_object('theme_options_table')
- self.colorbuttons = {}
- for chatstate in ('inactive', 'composing', 'paused', 'gone',
- 'muc_msg', 'muc_directed_msg'):
- self.colorbuttons[chatstate] = self.xml.get_object(chatstate + \
- '_colorbutton')
- model = Gtk.ListStore(str)
- self.themes_tree.set_model(model)
- col = Gtk.TreeViewColumn(_('Theme'))
- self.themes_tree.append_column(col)
- renderer = Gtk.CellRendererText()
- col.pack_start(renderer, True)
- col.add_attribute(renderer, 'text', 0)
- renderer.connect('edited', self.on_theme_cell_edited)
- renderer.set_property('editable', True)
- self.current_theme = app.config.get('roster_theme')
- self.no_update = False
- self.fill_themes_treeview()
- self.select_active_theme()
- self.current_option = self.options[0]
- self.set_theme_options(self.current_theme, self.current_option)
-
- self.xml.connect_signals(self)
- self.window.connect('delete-event', self.on_themese_window_delete_event)
- self.themes_tree.get_selection().connect('changed',
- self.selection_changed)
- self.window.show_all()
-
- def on_themese_window_delete_event(self, widget, event):
- self.window.hide()
- return True # do NOT destroy the window
-
- def on_close_button_clicked(self, widget):
- window = app.get_app_window('Preferences')
- if window is not None:
- window.update_theme_list()
- self.window.hide()
-
- def on_theme_cell_edited(self, cell, row, new_name):
- model = self.themes_tree.get_model()
- iter_ = model.get_iter_from_string(row)
- old_name = model.get_value(iter_, 0)
- if old_name == new_name:
- return
- if old_name == 'default':
- ErrorDialog(
- _('You cannot make changes to the default theme'),
- _('Please create a new clean theme.'))
- return
- new_config_name = new_name.replace(' ', '_')
- if new_config_name in app.config.get_per('themes'):
- return
- app.config.add_per('themes', new_config_name)
- # Copy old theme values
- old_config_name = old_name.replace(' ', '_')
- properties = ['textcolor', 'bgcolor', 'font', 'fontattrs']
- app.config.add_per('themes', new_config_name)
- for option in self.options:
- for property_ in properties:
- option_name = option + property_
- app.config.set_per('themes', new_config_name, option_name,
- app.config.get_per('themes', old_config_name, option_name))
- app.config.del_per('themes', old_config_name)
- if old_config_name == app.config.get('roster_theme'):
- app.config.set('roster_theme', new_config_name)
- model.set_value(iter_, 0, new_name)
- self.current_theme = new_name
-
- def fill_themes_treeview(self):
- model = self.themes_tree.get_model()
- model.clear()
- for config_theme in app.config.get_per('themes'):
- theme = config_theme.replace('_', ' ')
- model.append([theme])
-
- def select_active_theme(self):
- model = self.themes_tree.get_model()
- iter_ = model.get_iter_first()
- active_theme = app.config.get('roster_theme').replace('_', ' ')
- while iter_:
- theme = model[iter_][0]
- if theme == active_theme:
- self.themes_tree.get_selection().select_iter(iter_)
- if active_theme == 'default':
- self.xml.get_object('remove_button').set_sensitive(False)
- self.theme_options_vbox.set_sensitive(False)
- self.theme_options_table.set_sensitive(False)
- else:
- self.xml.get_object('remove_button').set_sensitive(True)
- self.theme_options_vbox.set_sensitive(True)
- self.theme_options_table.set_sensitive(True)
- break
- iter_ = model.iter_next(iter_)
-
- def selection_changed(self, widget = None):
- (model, iter_) = self.themes_tree.get_selection().get_selected()
- selected = self.themes_tree.get_selection().get_selected_rows()
- if not iter_ or selected[1] == []:
- self.theme_options_vbox.set_sensitive(False)
- self.theme_options_table.set_sensitive(False)
- return
- self.current_theme = model.get_value(iter_, 0)
- self.current_theme = self.current_theme.replace(' ', '_')
- self.set_theme_options(self.current_theme)
- if self.current_theme == 'default':
- self.xml.get_object('remove_button').set_sensitive(False)
- self.theme_options_vbox.set_sensitive(False)
- self.theme_options_table.set_sensitive(False)
- else:
- self.xml.get_object('remove_button').set_sensitive(True)
- self.theme_options_vbox.set_sensitive(True)
- self.theme_options_table.set_sensitive(True)
-
- def on_add_button_clicked(self, widget):
- model = self.themes_tree.get_model()
- iter_ = model.append()
- i = 0
- # don't confuse translators
- theme_name = _('theme name')
- theme_name_ns = theme_name.replace(' ', '_')
- while theme_name_ns + str(i) in app.config.get_per('themes'):
- i += 1
- model.set_value(iter_, 0, theme_name + str(i))
- app.config.add_per('themes', theme_name_ns + str(i))
- self.themes_tree.get_selection().select_iter(iter_)
- col = self.themes_tree.get_column(0)
- path = model.get_path(iter_)
- self.themes_tree.set_cursor(path, col, True)
-
- def on_remove_button_clicked(self, widget):
- (model, iter_) = self.themes_tree.get_selection().get_selected()
- if not iter_:
- return
- if self.current_theme == app.config.get('roster_theme'):
- ErrorDialog(
- _('You cannot delete your current theme'),
- _('Pick another theme to use first.'))
- return
- self.theme_options_vbox.set_sensitive(False)
- self.theme_options_table.set_sensitive(False)
- self.xml.get_object('remove_button').set_sensitive(False)
- app.config.del_per('themes', self.current_theme)
- model.remove(iter_)
-
- def set_theme_options(self, theme, option = 'account'):
- self.no_update = True
- self.options_combobox.set_active(self.options.index(option))
- textcolor = app.config.get_per('themes', theme, option + 'textcolor')
- if textcolor:
- state = True
- rgba = Gdk.RGBA()
- rgba.parse(textcolor)
- self.text_colorbutton.set_rgba(rgba)
- else:
- state = False
- self.textcolor_checkbutton.set_active(state)
- self.text_colorbutton.set_sensitive(state)
- bgcolor = app.config.get_per('themes', theme, option + 'bgcolor')
- if bgcolor:
- state = True
- rgba = Gdk.RGBA()
- rgba.parse(bgcolor)
- self.background_colorbutton.set_rgba(rgba)
- else:
- state = False
- self.background_checkbutton.set_active(state)
- self.background_colorbutton.set_sensitive(state)
-
- # get the font name before we set widgets and it will not be overridden
- font_name = app.config.get_per('themes', theme, option + 'font')
- font_attrs = app.config.get_per('themes', theme, option + 'fontattrs')
- self._set_font_widgets(font_attrs)
- if font_name:
- state = True
- self.text_fontbutton.set_font_name(font_name)
- else:
- state = False
- self.textfont_checkbutton.set_active(state)
- self.text_fontbutton.set_sensitive(state)
- self.no_update = False
- app.interface.roster.change_roster_style(None)
-
- for chatstate in ('inactive', 'composing', 'paused', 'gone',
- 'muc_msg', 'muc_directed_msg'):
- color = app.config.get_per('themes', theme, 'state_' + chatstate + \
- '_color')
- rgba = Gdk.RGBA()
- rgba.parse(color)
- self.colorbuttons[chatstate].set_rgba(rgba)
-
- def on_textcolor_checkbutton_toggled(self, widget):
- state = widget.get_active()
- self.text_colorbutton.set_sensitive(state)
- self._set_color(state, self.text_colorbutton,
- 'textcolor')
-
- def on_background_checkbutton_toggled(self, widget):
- state = widget.get_active()
- self.background_colorbutton.set_sensitive(state)
- self._set_color(state, self.background_colorbutton,
- 'bgcolor')
-
- def on_textfont_checkbutton_toggled(self, widget):
- self.text_fontbutton.set_sensitive(widget.get_active())
- self._set_font()
-
- def on_text_colorbutton_color_set(self, widget):
- self._set_color(True, widget, 'textcolor')
-
- def on_background_colorbutton_color_set(self, widget):
- self._set_color(True, widget, 'bgcolor')
-
- def on_text_fontbutton_font_set(self, widget):
- self._set_font()
-
- def on_options_combobox_changed(self, widget):
- index = self.options_combobox.get_active()
- if index == -1:
- return
- self.current_option = self.options[index]
- self.set_theme_options(self.current_theme,
- self.current_option)
-
- def on_bold_togglebutton_toggled(self, widget):
- if not self.no_update:
- self._set_font()
-
- def on_italic_togglebutton_toggled(self, widget):
- if not self.no_update:
- self._set_font()
-
- def _set_color(self, state, widget, option):
- """
- Set color value in prefs and update the UI
- """
- if state:
- color = widget.get_rgba()
- color_string = color.to_string()
- else:
- color_string = ''
- begin_option = ''
- if not option.startswith('state'):
- begin_option = self.current_option
- app.config.set_per('themes', self.current_theme,
- begin_option + option, color_string)
- # use faster functions for this
- if self.current_option == 'banner':
- app.interface.roster.repaint_themed_widgets()
- return
- if self.no_update:
- return
- app.interface.roster.change_roster_style(self.current_option)
-
- def _set_font(self):
- """
- Set font value in prefs and update the UI
- """
- state = self.textfont_checkbutton.get_active()
- if state:
- font_string = self.text_fontbutton.get_font_name()
- else:
- font_string = ''
- app.config.set_per('themes', self.current_theme,
- self.current_option + 'font', font_string)
- font_attrs = self._get_font_attrs()
- app.config.set_per('themes', self.current_theme,
- self.current_option + 'fontattrs', font_attrs)
- # use faster functions for this
- if self.current_option == 'banner':
- app.interface.roster.repaint_themed_widgets()
- if self.no_update:
- return
- app.interface.roster.change_roster_style(self.current_option)
-
- def _toggle_font_widgets(self, font_props):
- """
- Toggle font buttons with the bool values of font_props tuple
- """
- self.bold_togglebutton.set_active(font_props[0])
- self.italic_togglebutton.set_active(font_props[1])
-
- def _get_font_description(self):
- """
- Return a FontDescription from togglebuttons states
- """
- fd = Pango.FontDescription()
- if self.bold_togglebutton.get_active():
- fd.set_weight(Pango.Weight.BOLD)
- if self.italic_togglebutton.get_active():
- fd.set_style(Pango.Style.ITALIC)
- return fd
-
- def _set_font_widgets(self, font_attrs):
- """
- Set the correct toggle state of font style buttons by a font string of
- type 'BI'
- """
- font_props = [False, False, False]
- if font_attrs:
- if font_attrs.find('B') != -1:
- font_props[0] = True
- if font_attrs.find('I') != -1:
- font_props[1] = True
- self._toggle_font_widgets(font_props)
-
- def _get_font_attrs(self):
- """
- Get a string with letters of font attribures: 'BI'
- """
- attrs = ''
- if self.bold_togglebutton.get_active():
- attrs += 'B'
- if self.italic_togglebutton.get_active():
- attrs += 'I'
- return attrs
-
-
- def _get_font_props(self, font_name):
- """
- Get tuple of font properties: weight, style
- """
- font_props = [False, False, False]
- font_description = Pango.FontDescription(font_name)
- if font_description.get_weight() != Pango.Weight.NORMAL:
- font_props[0] = True
- if font_description.get_style() != Pango.Style.ITALIC:
- font_props[1] = True
- return font_props
-
- def on_inactive_colorbutton_color_set(self, widget):
- self.no_update = True
- self._set_color(True, widget, 'state_inactive_color')
- self.no_update = False
-
- def on_composing_colorbutton_color_set(self, widget):
- self.no_update = True
- self._set_color(True, widget, 'state_composing_color')
- self.no_update = False
-
- def on_paused_colorbutton_color_set(self, widget):
- self.no_update = True
- self._set_color(True, widget, 'state_paused_color')
- self.no_update = False
-
- def on_gone_colorbutton_color_set(self, widget):
- self.no_update = True
- self._set_color(True, widget, 'state_gone_color')
- self.no_update = False
-
- def on_muc_msg_colorbutton_color_set(self, widget):
- self.no_update = True
- self._set_color(True, widget, 'state_muc_msg_color')
- self.no_update = False
-
- def on_muc_directed_msg_colorbutton_color_set(self, widget):
- self.no_update = True
- self._set_color(True, widget, 'state_muc_directed_msg_color')
- self.no_update = False
diff --git a/gajim/groupchat_control.py b/gajim/groupchat_control.py
index ef442edf6..3cb5f8a14 100644
--- a/gajim/groupchat_control.py
+++ b/gajim/groupchat_control.py
@@ -59,6 +59,7 @@ from gajim.common.modules import dataforms
from gajim.common import ged
from gajim.common import i18n
from gajim.common import contacts
+from gajim.common.const import StyleAttr
from gajim.chat_control import ChatControl
from gajim.chat_control_base import ChatControlBase
@@ -95,11 +96,11 @@ def cell_data_func(column, renderer, model, iter_, user_data):
theme = app.config.get('roster_theme')
has_parent = bool(model.iter_parent(iter_))
if has_parent:
- bgcolor = app.config.get_per('themes', theme, 'contactbgcolor')
- renderer.set_property('cell-background', bgcolor or None)
+ bgcolor = app.css_config.get_value('.gajim-contact-row', StyleAttr.BACKGROUND)
+ renderer.set_property('cell-background', bgcolor)
else:
- bgcolor = app.config.get_per('themes', theme, 'groupbgcolor')
- renderer.set_property('cell-background', bgcolor or None)
+ bgcolor = app.css_config.get_value('.gajim-group-row', StyleAttr.BACKGROUND)
+ renderer.set_property('cell-background', bgcolor)
if user_data == 'status':
status_cell_data_func(column, renderer, model, iter_, has_parent)
@@ -133,18 +134,15 @@ def text_cell_data_func(column, renderer, model, iter_, has_parent, theme):
# cell data func is global, because we don't want it to keep
# reference to GroupchatControl instance (self)
if has_parent:
- color = app.config.get_per('themes', theme, 'contacttextcolor')
- if color:
- renderer.set_property('foreground', color)
- else:
- renderer.set_property('foreground', None)
- renderer.set_property('font',
- gtkgui_helpers.get_theme_font_for_option(theme, 'contactfont'))
+ color = app.css_config.get_value('.gajim-contact-row', StyleAttr.COLOR)
+ renderer.set_property('foreground', color)
+ desc = app.css_config.get_font('.gajim-contact-row')
+ renderer.set_property('font-desc', desc)
else:
- color = app.config.get_per('themes', theme, 'grouptextcolor')
- renderer.set_property('foreground', color or None)
- renderer.set_property('font',
- gtkgui_helpers.get_theme_font_for_option(theme, 'groupfont'))
+ color = app.css_config.get_value('.gajim-group-row', StyleAttr.COLOR)
+ renderer.set_property('foreground', color)
+ desc = app.css_config.get_font('.gajim-group-row')
+ renderer.set_property('font-desc', desc)
class PrivateChatControl(ChatControl):
@@ -1023,14 +1021,14 @@ class GroupchatControl(ChatControlBase):
color = None
if chatstate == 'attention' and (not has_focus or not current_tab):
self.attention_flag = True
- color = 'state_muc_directed_msg_color'
+ color = 'tab-muc-directed-msg'
elif chatstate == 'active' or (current_tab and has_focus):
self.attention_flag = False
# get active color from gtk
color = 'active'
elif chatstate == 'newmsg' and (not has_focus or not current_tab) \
and not self.attention_flag:
- color = 'state_muc_msg_color'
+ color = 'tab-muc-msg'
if self.is_continued:
# if this is a continued conversation
@@ -1146,18 +1144,16 @@ class GroupchatControl(ChatControlBase):
room jid, subject
"""
self.name_label.set_ellipsize(Pango.EllipsizeMode.END)
- font_attrs, font_attrs_small = self.get_font_attrs()
if self.is_continued:
name = self.get_continued_conversation_name()
else:
name = self.room_jid
- text = '%s' % (font_attrs, name)
- self.name_label.set_markup(text)
+
+ self.name_label.set_text(name)
if self.subject:
subject = GLib.markup_escape_text(self.subject)
subject_text = self.urlfinder.sub(self.make_href, subject)
- subject_text = '%s' % subject_text
self.subject_button.get_popover().set_text(subject_text)
def _nec_vcard_published(self, obj):
@@ -1719,13 +1715,8 @@ class GroupchatControl(ChatControlBase):
if status != '':
status = helpers.reduce_chars_newlines(status, max_lines=1)
# escape markup entities and make them small italic and fg color
- color = gtkgui_helpers.get_fade_color(self.list_treeview,
- selected, focus)
- colorstring = "#%04x%04x%04x" % (int(color.red * 65535),
- int(color.green * 65535), int(color.blue * 65535))
- name += ('\n'
- '%s') % (colorstring, GLib.markup_escape_text(
- status))
+ name += ('\n'
+ '{}'.format(GLib.markup_escape_text(status)))
if (gc_contact.affiliation != 'none' and
app.config.get('show_affiliation_in_groupchat')):
@@ -2112,7 +2103,7 @@ class GroupchatControl(ChatControlBase):
if self.correcting:
self.correcting = False
gtkgui_helpers.remove_css_class(
- self.msg_textview, 'msgcorrectingcolor')
+ self.msg_textview, 'gajim-msg-correcting')
def send_message(self, message, xhtml=None, process_commands=True):
"""
diff --git a/gajim/gtk/__init__.py b/gajim/gtk/__init__.py
index f26941488..6b9444a80 100644
--- a/gajim/gtk/__init__.py
+++ b/gajim/gtk/__init__.py
@@ -52,6 +52,7 @@ from gajim.gtk.dialogs import AspellDictError
from gajim.gtk.dialogs import HigDialog
from gajim.gtk.dialogs import SSLErrorDialog
from gajim.gtk.dialogs import ChangePasswordDialog
+from gajim.gtk.dialogs import NewConfirmationDialog
from gajim.gtk.about import AboutDialog
from gajim.gtk.join_groupchat import JoinGroupchatWindow
diff --git a/gajim/gtk/dialogs.py b/gajim/gtk/dialogs.py
index 62b693ae6..fb31e8c27 100644
--- a/gajim/gtk/dialogs.py
+++ b/gajim/gtk/dialogs.py
@@ -986,3 +986,42 @@ class ChangePasswordDialog(Gtk.Dialog):
self._error_label.set_text(error_text)
self._password1_entry.set_sensitive(True)
self._password2_entry.set_sensitive(True)
+
+
+class NewConfirmationDialog(Gtk.MessageDialog):
+ def __init__(self, text, sec_text, buttons, transient_for=None):
+ Gtk.MessageDialog.__init__(self,
+ transient_for=transient_for,
+ message_type=Gtk.MessageType.QUESTION,
+ text=text)
+
+ self._buttons = buttons
+
+ for response, button in buttons.items():
+ self.add_button(button.text, response)
+ if button.action is not None:
+ widget = self.get_widget_for_response(response)
+ widget.get_style_context().add_class(button.action.value)
+
+ self.format_secondary_markup(sec_text)
+
+ self.connect('response', self._on_response)
+
+ self.run()
+
+ def _on_response(self, dialog, response):
+ if response == Gtk.ResponseType.DELETE_EVENT:
+ # Look if DELETE_EVENT is mapped to another response
+ response = self._buttons.get(response, None)
+ if response is None:
+ # If DELETE_EVENT was not mapped we assume CANCEL
+ response = Gtk.ResponseType.CANCEL
+
+ button = self._buttons.get(response, None)
+ if button is None:
+ self.destroy()
+ return
+
+ if button.callback is not None:
+ button.callback()
+ self.destroy()
diff --git a/gajim/gtk/preferences.py b/gajim/gtk/preferences.py
index f65c26b3f..1b90885d9 100644
--- a/gajim/gtk/preferences.py
+++ b/gajim/gtk/preferences.py
@@ -25,7 +25,7 @@ from gajim.common import config as c_config
from gajim.common import idle
from gajim.gtk.util import get_builder
from gajim.gtk import AspellDictError
-from gajim.gajim_themes_window import GajimThemesWindow
+from gajim.gtk.themes import Themes
from gajim.advanced_configuration_window import AdvancedConfigurationWindow
from gajim.chat_control_base import ChatControlBase
from gajim.config import ManageProxiesWindow, ManageSoundsWindow
@@ -160,9 +160,8 @@ class Preferences(Gtk.ApplicationWindow):
### Style tab ###
# Themes
theme_combobox = self.xml.get_object('theme_combobox')
- cell = Gtk.CellRendererText()
- theme_combobox.pack_start(cell, True)
- theme_combobox.add_attribute(cell, 'text', 0)
+ self.changed_id = theme_combobox.connect(
+ 'changed', self.on_theme_combobox_changed)
self.update_theme_list()
# iconset
@@ -207,19 +206,6 @@ class Preferences(Gtk.ApplicationWindow):
st = app.config.get('use_transports_iconsets')
self.xml.get_object('transports_iconsets_checkbutton').set_active(st)
- # Color widgets
- self.draw_color_widgets()
-
- # Font for messages
- font = app.config.get('conversation_font')
- # try to set default font for the current desktop env
- fontbutton = self.xml.get_object('conversation_fontbutton')
- if font == '':
- fontbutton.set_sensitive(False)
- self.xml.get_object('default_chat_font').set_active(True)
- else:
- fontbutton.set_font_name(font)
-
### Personal Events tab ###
# outgoing send chat state notifications
st = app.config.get('outgoing_chat_state_notifications')
@@ -473,8 +459,8 @@ class Preferences(Gtk.ApplicationWindow):
self.default_msg_tree.get_model().connect('row-changed',
self.on_default_msg_treemodel_row_changed)
- self.theme_preferences = None
self.sounds_preferences = None
+ self.theme_preferences = None
self.notebook.set_current_page(0)
@@ -625,36 +611,32 @@ class Preferences(Gtk.ApplicationWindow):
def on_show_avatar_in_tabs_checkbutton_toggled(self, widget):
self.on_checkbutton_toggled(widget, 'show_avatar_in_tabs')
- def on_theme_combobox_changed(self, widget):
- model = widget.get_model()
- active = widget.get_active()
- config_theme = model[active][0].replace(' ', '_')
-
- app.config.set('roster_theme', config_theme)
+ @staticmethod
+ def on_theme_combobox_changed(combobox):
+ theme = combobox.get_active_id()
+ app.config.set('roster_theme', theme)
+ app.css_config.change_theme(theme)
# begin repainting themed widgets throughout
app.interface.roster.repaint_themed_widgets()
app.interface.roster.change_roster_style(None)
- gtkgui_helpers.load_css()
def update_theme_list(self):
theme_combobox = self.xml.get_object('theme_combobox')
- model = Gtk.ListStore(str)
- theme_combobox.set_model(model)
- i = 0
- for config_theme in app.config.get_per('themes'):
- theme = config_theme.replace('_', ' ')
- model.append([theme])
- if app.config.get('roster_theme') == config_theme:
- theme_combobox.set_active(i)
- i += 1
+ with theme_combobox.handler_block(self.changed_id):
+ theme_combobox.remove_all()
+ theme_combobox.append('default', 'default')
+ for config_theme in app.css_config.themes:
+ theme_combobox.append(config_theme, config_theme)
+
+ theme_combobox.set_active_id(app.config.get('roster_theme'))
def on_manage_theme_button_clicked(self, widget):
- if self.theme_preferences is None:
- self.theme_preferences = GajimThemesWindow(self)
+ window = app.get_app_window(Themes)
+ if window is None:
+ Themes(self)
else:
- self.theme_preferences.window.present()
- self.theme_preferences.select_active_theme()
+ window.present()
def on_iconset_combobox_changed(self, widget):
model = widget.get_model()
@@ -747,128 +729,6 @@ class Preferences(Gtk.ApplicationWindow):
else:
self.sounds_preferences.window.present()
- def update_text_tags(self):
- """
- Update color tags in opened chat windows
- """
- for ctrl in self._get_all_controls():
- ctrl.update_tags()
-
- def on_preference_widget_color_set(self, widget, text):
- color = widget.get_color()
- color_string = color.to_string()
- app.config.set(text, color_string)
- self.update_text_tags()
-
- def on_preference_widget_font_set(self, widget, text):
- if widget:
- font = widget.get_font_name()
- else:
- font = ''
- app.config.set(text, font)
- gtkgui_helpers.load_css()
-
- def on_incoming_nick_colorbutton_color_set(self, widget):
- self.on_preference_widget_color_set(widget, 'inmsgcolor')
-
- def on_outgoing_nick_colorbutton_color_set(self, widget):
- self.on_preference_widget_color_set(widget, 'outmsgcolor')
-
- def on_incoming_msg_colorbutton_color_set(self, widget):
- self.on_preference_widget_color_set(widget, 'inmsgtxtcolor')
-
- def on_outgoing_msg_colorbutton_color_set(self, widget):
- self.on_preference_widget_color_set(widget, 'outmsgtxtcolor')
-
- def on_url_msg_colorbutton_color_set(self, widget):
- self.on_preference_widget_color_set(widget, 'urlmsgcolor')
-
- def on_status_msg_colorbutton_color_set(self, widget):
- self.on_preference_widget_color_set(widget, 'statusmsgcolor')
-
- def on_muc_highlight_colorbutton_color_set(self, widget):
- self.on_preference_widget_color_set(widget, 'markedmsgcolor')
-
- def on_conversation_fontbutton_font_set(self, widget):
- self.on_preference_widget_font_set(widget, 'conversation_font')
-
- def on_default_chat_font_toggled(self, widget):
- font_widget = self.xml.get_object('conversation_fontbutton')
- if widget.get_active():
- font_widget.set_sensitive(False)
- font_widget = None
- else:
- font_widget.set_sensitive(True)
- self.on_preference_widget_font_set(font_widget, 'conversation_font')
-
- def draw_color_widgets(self):
- col_to_widget = {'inmsgcolor': 'incoming_nick_colorbutton',
- 'outmsgcolor': 'outgoing_nick_colorbutton',
- 'inmsgtxtcolor': ['incoming_msg_colorbutton',
- 'incoming_msg_checkbutton'],
- 'outmsgtxtcolor': ['outgoing_msg_colorbutton',
- 'outgoing_msg_checkbutton'],
- 'statusmsgcolor': 'status_msg_colorbutton',
- 'urlmsgcolor': 'url_msg_colorbutton',
- 'markedmsgcolor': 'muc_highlight_colorbutton'}
- for c in col_to_widget:
- col = app.config.get(c)
- if col:
- if isinstance(col_to_widget[c], list):
- rgba = Gdk.RGBA()
- rgba.parse(col)
- self.xml.get_object(col_to_widget[c][0]).set_rgba(rgba)
- self.xml.get_object(col_to_widget[c][0]).set_sensitive(True)
- self.xml.get_object(col_to_widget[c][1]).set_active(True)
- else:
- rgba = Gdk.RGBA()
- rgba.parse(col)
- self.xml.get_object(col_to_widget[c]).set_rgba(rgba)
- else:
- rgba = Gdk.RGBA()
- rgba.parse('#000000')
- if isinstance(col_to_widget[c], list):
- self.xml.get_object(col_to_widget[c][0]).set_rgba(rgba)
- self.xml.get_object(col_to_widget[c][0]).set_sensitive(False)
- self.xml.get_object(col_to_widget[c][1]).set_active(False)
- else:
- self.xml.get_object(col_to_widget[c]).set_rgba(rgba)
-
- def on_reset_colors_button_clicked(self, widget):
- col_to_widget = {'inmsgcolor': 'incoming_nick_colorbutton',
- 'outmsgcolor': 'outgoing_nick_colorbutton',
- 'inmsgtxtcolor': 'incoming_msg_colorbutton',
- 'outmsgtxtcolor': 'outgoing_msg_colorbutton',
- 'statusmsgcolor': 'status_msg_colorbutton',
- 'urlmsgcolor': 'url_msg_colorbutton',
- 'markedmsgcolor': 'muc_highlight_colorbutton'}
- for c in col_to_widget:
- app.config.set(c, app.interface.default_colors[c])
- self.draw_color_widgets()
-
- self.update_text_tags()
-
- def _set_color(self, state, widget_name, option):
- """
- Set color value in prefs and update the UI
- """
- if state:
- color = self.xml.get_object(widget_name).get_rgba()
- color_string = color.to_string()
- else:
- color_string = ''
- app.config.set(option, color_string)
-
- def on_incoming_msg_checkbutton_toggled(self, widget):
- state = widget.get_active()
- self.xml.get_object('incoming_msg_colorbutton').set_sensitive(state)
- self._set_color(state, 'incoming_msg_colorbutton', 'inmsgtxtcolor')
-
- def on_outgoing_msg_checkbutton_toggled(self, widget):
- state = widget.get_active()
- self.xml.get_object('outgoing_msg_colorbutton').set_sensitive(state)
- self._set_color(state, 'outgoing_msg_colorbutton', 'outmsgtxtcolor')
-
def on_auto_away_checkbutton_toggled(self, widget):
self.on_checkbutton_toggled(widget, 'autoaway',
[self.auto_away_time_spinbutton, self.auto_away_message_entry])
diff --git a/gajim/gtk/themes.py b/gajim/gtk/themes.py
new file mode 100644
index 000000000..0c86ec485
--- /dev/null
+++ b/gajim/gtk/themes.py
@@ -0,0 +1,415 @@
+# 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.const import StyleAttr, DialogButton, ButtonAction
+from gajim.common.connection_handlers_events import StyleChanged
+from gajim.gtk import ErrorDialog
+from gajim.gtk 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 Color'),
+ '.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 Color'),
+ '.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 Color'),
+ '.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
+ else:
+ return other._option == self._option
+
+
+class ChooseOption(Gtk.ListBoxRow):
+ def __init__(self, option):
+ Gtk.ListBoxRow.__init__(self)
+ self.option = option
+ label = Gtk.Label(option.label)
+ label.set_xalign(0)
+ self.add(label)
+ self.show_all()
diff --git a/gajim/gtk/util.py b/gajim/gtk/util.py
index 8eb9018dd..4c918f6b4 100644
--- a/gajim/gtk/util.py
+++ b/gajim/gtk/util.py
@@ -207,3 +207,14 @@ def python_month(month):
def gtk_month(month):
return month - 1
+
+
+def convert_rgb_to_hex(rgb_string):
+ rgb = Gdk.RGBA()
+ rgb.parse(rgb_string)
+ rgb.to_color()
+
+ red = int(rgb.red * 255)
+ green = int(rgb.green * 255)
+ blue = int(rgb.blue * 255)
+ return '#%02x%02x%02x' % (red, green, blue)
diff --git a/gajim/gtk/xml_console.py b/gajim/gtk/xml_console.py
index 6f1700e01..ccbd8a8a6 100644
--- a/gajim/gtk/xml_console.py
+++ b/gajim/gtk/xml_console.py
@@ -21,7 +21,7 @@ from gi.repository import GLib
from gajim.common import app
from gajim.common import ged
-from gajim.common.const import Option, OptionKind, OptionType
+from gajim.common.const import Option, OptionKind, OptionType, StyleAttr
from gajim.gtk import ErrorDialog
from gajim.gtk import util
from gajim.gtk.util import get_builder
@@ -97,8 +97,10 @@ class XMLConsoleWindow(Gtk.Window):
def create_tags(self):
buffer_ = self.textview.get_buffer()
- in_color = app.config.get('inmsgcolor')
- out_color = app.config.get('outmsgcolor')
+ in_color = app.css_config.get_value(
+ '.gajim-incoming-nickname', StyleAttr.COLOR)
+ out_color = app.css_config.get_value(
+ '.gajim-outgoing-nickname', StyleAttr.COLOR)
tags = ['presence', 'message', 'stream', 'iq']
diff --git a/gajim/gtkgui_helpers.py b/gajim/gtkgui_helpers.py
index a80dad572..b170f0fe8 100644
--- a/gajim/gtkgui_helpers.py
+++ b/gajim/gtkgui_helpers.py
@@ -38,6 +38,7 @@ import os
import sys
import math
import xml.etree.ElementTree as ET
+
try:
from PIL import Image
except:
@@ -172,22 +173,6 @@ def get_completion_liststore(entry):
entry.set_completion(completion)
return liststore
-def get_theme_font_for_option(theme, option):
- """
- Return string description of the font, stored in theme preferences
- """
- font_name = app.config.get_per('themes', theme, option)
- font_desc = Pango.FontDescription()
- font_prop_str = app.config.get_per('themes', theme, option + 'attrs')
- if font_prop_str:
- if font_prop_str.find('B') != -1:
- font_desc.set_weight(Pango.Weight.BOLD)
- if font_prop_str.find('I') != -1:
- font_desc.set_style(Pango.Style.ITALIC)
- fd = Pango.FontDescription(font_name)
- fd.merge(font_desc, True)
- return fd.to_string()
-
def move_window(window, x, y):
"""
Move the window, but also check if out of screen
@@ -812,71 +797,19 @@ def __label_size_allocate(widget, allocation):
def get_action(action):
return app.app.lookup_action(action)
-def load_css():
- path = os.path.join(configpaths.get('DATA'), 'style', 'gajim.css')
- try:
- with open(path, "r") as f:
- css = f.read()
- except Exception as exc:
- print('Error loading css: %s', exc)
- return
+def add_css_class(widget, class_name, prefix=None):
+ if class_name and prefix:
+ class_name = prefix + class_name
- provider = Gtk.CssProvider()
- css = "\n".join((css, convert_config_to_css()))
- provider.load_from_data(bytes(css.encode()))
- Gtk.StyleContext.add_provider_for_screen(
- Gdk.Screen.get_default(),
- provider,
- Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION)
-
-def convert_config_to_css():
- css = ''
- themed_widgets = {
- 'ChatControl-BannerEventBox': ('bannerbgcolor', 'background'),
- 'ChatControl-BannerNameLabel': ('bannertextcolor', 'color'),
- 'ChatControl-BannerLabel': ('bannertextcolor', 'color'),
- 'GroupChatControl-BannerEventBox': ('bannerbgcolor', 'background'),
- 'GroupChatControl-BannerNameLabel': ('bannertextcolor', 'color'),
- 'GroupChatControl-BannerLabel': ('bannertextcolor', 'color'),
- 'Discovery-BannerEventBox': ('bannerbgcolor', 'background'),
- 'Discovery-BannerLabel': ('bannertextcolor', 'color')}
-
- classes = {'state_composing_color': ('', 'color'),
- 'state_inactive_color': ('', 'color'),
- 'state_gone_color': ('', 'color'),
- 'state_paused_color': ('', 'color'),
- 'msgcorrectingcolor': ('text', 'background'),
- 'state_muc_directed_msg_color': ('', 'color'),
- 'state_muc_msg_color': ('', 'color')}
-
-
- theme = app.config.get('roster_theme')
- for key, values in themed_widgets.items():
- config, attr = values
- css += '#{} {{'.format(key)
- value = app.config.get_per('themes', theme, config)
- if value:
- css += '{attr}: {color};\n'.format(attr=attr, color=value)
- css += '}\n'
-
- for key, values in classes.items():
- node, attr = values
- value = app.config.get_per('themes', theme, key)
- if value:
- css += '.theme_{cls} {node} {{ {attr}: {color}; }}\n'.format(
- cls=key, node=node, attr=attr, color=value)
-
- css += add_css_font()
-
- return css
-
-def add_css_class(widget, class_name):
style = widget.get_style_context()
- for css_cls in style.list_classes():
- if css_cls.startswith('theme_'):
- style.remove_class(css_cls)
- if class_name:
- style.add_class('theme_' + class_name)
+ if prefix is not None:
+ # Remove all css classes with prefix
+ for css_cls in style.list_classes():
+ if css_cls.startswith(prefix):
+ style.remove_class(css_cls)
+
+ if class_name is not None:
+ style.add_class(class_name)
def add_css_to_widget(widget, css):
provider = Gtk.CssProvider()
@@ -887,28 +820,7 @@ def add_css_to_widget(widget, css):
def remove_css_class(widget, class_name):
style = widget.get_style_context()
- style.remove_class('theme_' + class_name)
-
-def add_css_font():
- conversation_font = app.config.get('conversation_font')
- if not conversation_font:
- return ''
- font = Pango.FontDescription(conversation_font)
- unit = "pt" if Gtk.check_version(3, 22, 0) is None else "px"
- css = """
- .font_custom {{
- font-family: "{family}";
- font-size: {size}{unit};
- font-weight: {weight};
- }}""".format(
- family=font.get_family(),
- size=int(round(font.get_size() / Pango.SCALE)),
- unit=unit,
- weight=pango_to_css_weight(font.get_weight()))
- css = css.replace("font-size: 0{unit};".format(unit=unit), "")
- css = css.replace("font-weight: 0;", "")
- css = "\n".join(filter(lambda x: x.strip(), css.splitlines()))
- return css
+ style.remove_class(class_name)
def draw_affiliation(surface, affiliation):
icon_size = 16
diff --git a/gajim/gui_interface.py b/gajim/gui_interface.py
index 4c6e16823..aa08ae09d 100644
--- a/gajim/gui_interface.py
+++ b/gajim/gui_interface.py
@@ -2655,15 +2655,6 @@ class Interface:
self.gpg_passphrase = {}
self.pass_dialog = {}
self.db_error_dialog = None
- self.default_colors = {
- 'inmsgcolor': app.config.get('inmsgcolor'),
- 'outmsgcolor': app.config.get('outmsgcolor'),
- 'inmsgtxtcolor': app.config.get('inmsgtxtcolor'),
- 'outmsgtxtcolor': app.config.get('outmsgtxtcolor'),
- 'statusmsgcolor': app.config.get('statusmsgcolor'),
- 'urlmsgcolor': app.config.get('urlmsgcolor'),
- 'markedmsgcolor': app.config.get('markedmsgcolor'),
- }
self.handlers = {}
self.roster = None
@@ -2684,6 +2675,9 @@ class Interface:
# enable plugin_installer by default when creating config file
app.config.set_per('plugins', 'plugin_installer', 'active', True)
+ # Load CSS files
+ app.load_css_config()
+
app.logger.reset_shown_unread_messages()
# override logging settings from config (don't take care of '-q' option)
if app.config.get('verbose'):
@@ -2721,24 +2715,7 @@ class Interface:
default[msg][4])
app.config.set_per('statusmsg', msg, 'mood_text',
default[msg][5])
- #add default themes if there is not in the config file
- theme = app.config.get('roster_theme')
- if not theme in app.config.get_per('themes'):
- app.config.set('roster_theme', _('default'))
- if len(app.config.get_per('themes')) == 0:
- d = ['accounttextcolor', 'accountbgcolor', 'accountfont',
- 'accountfontattrs', 'grouptextcolor', 'groupbgcolor',
- 'groupfont', 'groupfontattrs', 'contacttextcolor',
- 'contactbgcolor', 'contactfont', 'contactfontattrs',
- 'bannertextcolor', 'bannerbgcolor']
- default = app.config.themes_default
- for theme_name in default:
- app.config.add_per('themes', theme_name)
- theme = default[theme_name]
- for o in d:
- app.config.set_per('themes', theme_name, o,
- theme[d.index(o)])
# Add Tor proxy if there is not in the config
if len(app.config.get_per('proxies')) == 0:
default = app.config.proxies_default
diff --git a/gajim/history_manager.py b/gajim/history_manager.py
index befff38d6..a87157e6b 100644
--- a/gajim/history_manager.py
+++ b/gajim/history_manager.py
@@ -48,7 +48,7 @@ from gi.repository import Gio
from gajim.common import i18n
from gajim.common import configpaths
-
+from gajim.common.const import StyleAttr
def is_standalone():
# Determine if we are in standalone mode
@@ -395,14 +395,14 @@ class HistoryManager:
if kind in (KindConstant.SINGLE_MSG_RECV,
KindConstant.CHAT_MSG_RECV, KindConstant.GC_MSG):
# it is the other side
- color = app.config.get('inmsgcolor') # so incoming color
+ color = app.css_config.get_value('.gajim-incoming-nickname', StyleAttr.COLOR) # so incoming color
elif kind in (KindConstant.SINGLE_MSG_SENT,
KindConstant.CHAT_MSG_SENT): # it is us
- color = app.config.get('outmsgcolor') # so outgoing color
+ color = app.css_config.get_value('.gajim-outgoing-nickname', StyleAttr.COLOR) # so outgoing color
elif kind in (KindConstant.STATUS,
KindConstant.GCSTATUS): # is is statuses
# so status color
- color = app.config.get('statusmsgcolor')
+ color = app.css_config.get_value('.gajim-status-message', StyleAttr.COLOR)
# include status into (status) message
if message is None:
message = ''
diff --git a/gajim/htmltextview.py b/gajim/htmltextview.py
index 1958a01e0..75e5e9b82 100644
--- a/gajim/htmltextview.py
+++ b/gajim/htmltextview.py
@@ -57,6 +57,8 @@ from gajim.gtk.util import get_builder
from gajim.common import helpers
from gajim.gtk import JoinGroupchatWindow
from gajim.gtk import AddNewContactWindow
+from gajim.common.const import StyleAttr
+
import logging
log = logging.getLogger('gajim.htmlview')
@@ -514,7 +516,7 @@ class HtmlHandler(xml.sax.handler.ContentHandler):
tag.href = href
tag.type_ = type_ # to be used by the URL handler
tag.connect('event', self.textview.hyperlink_handler, 'url')
- tag.set_property('foreground', app.config.get('urlmsgcolor'))
+ tag.set_property('foreground', app.css_config.get_value('.gajim-url', StyleAttr.COLOR))
tag.set_property('underline', Pango.Underline.SINGLE)
tag.is_anchor = True
if title:
@@ -855,7 +857,7 @@ class HtmlTextView(Gtk.TextView):
buffer_ = self.get_buffer()
self.tagURL = buffer_.create_tag('url')
- color = app.config.get('urlmsgcolor')
+ color = app.css_config.get_value('.gajim-url', StyleAttr.COLOR)
self.tagURL.set_property('foreground', color)
self.tagURL.set_property('underline', Pango.Underline.SINGLE)
self.tagURL.connect('event', self.hyperlink_handler, 'url')
diff --git a/gajim/message_textview.py b/gajim/message_textview.py
index ad090bf28..0b099e9c5 100644
--- a/gajim/message_textview.py
+++ b/gajim/message_textview.py
@@ -55,7 +55,7 @@ class MessageTextView(Gtk.TextView):
self.set_right_margin(2)
self.set_pixels_above_lines(2)
self.set_pixels_below_lines(2)
- self.get_style_context().add_class('font_custom')
+ self.get_style_context().add_class('gajim-conversation-font')
# set undo list
self.undo_list = []
diff --git a/gajim/message_window.py b/gajim/message_window.py
index cc417ce31..1e9818d72 100644
--- a/gajim/message_window.py
+++ b/gajim/message_window.py
@@ -643,15 +643,14 @@ class MessageWindow(object):
if isinstance(ctrl, ChatControl):
tab_label_str = ctrl.get_tab_label()
# Set Label Color
- class_name = 'state_{}_color'.format(chatstate)
- gtkgui_helpers.add_css_class(nick_label, class_name)
+ gtkgui_helpers.add_css_class(nick_label, chatstate, 'gajim-state-')
else:
tab_label_str, color = ctrl.get_tab_label(chatstate)
# Set Label Color
if color == 'active':
- gtkgui_helpers.add_css_class(nick_label, None)
+ gtkgui_helpers.add_css_class(nick_label, None, 'gajim-state-')
elif color is not None:
- gtkgui_helpers.add_css_class(nick_label, color)
+ gtkgui_helpers.add_css_class(nick_label, color, 'gajim-state-')
nick_label.set_markup(tab_label_str)
diff --git a/gajim/roster_window.py b/gajim/roster_window.py
index f1ef36885..769b06bfb 100644
--- a/gajim/roster_window.py
+++ b/gajim/roster_window.py
@@ -57,6 +57,7 @@ from gajim import tooltips
from gajim import message_control
from gajim import adhoc_commands
from gajim.accounts_window import AccountsWindow
+
from gajim.gtk import JoinGroupchatWindow
from gajim.gtk import ConfirmationDialogCheck
from gajim.gtk import ConfirmationDialog
@@ -73,14 +74,12 @@ from gajim.gtk import AccountCreationWizard
from gajim.gtk import ServiceRegistration
from gajim.gtk import HistoryWindow
-from gajim.common.const import AvatarSize
-
from gajim.common import app
from gajim.common import helpers
from gajim.common import idle
from gajim.common.exceptions import GajimGeneralException
from gajim.common import i18n
-from gajim.common.const import PEPEventType
+from gajim.common.const import PEPEventType, AvatarSize, StyleAttr
if app.is_installed('GEOCLUE'):
from gajim.common import location_listener
from gajim.common import ged
@@ -1265,15 +1264,11 @@ class RosterWindow:
status = helpers.reduce_chars_newlines(status,
max_lines = 1)
# escape markup entities and make them small
- # italic and fg color color is calculated to be
- # always readable
- color = gtkgui_helpers.get_fade_color(self.tree, selected,
- focus)
- colorstring = '#%04x%04x%04x' % (int(color.red * 65535),
- int(color.green * 65535), int(color.blue * 65535))
- name += '\n%s' % (colorstring,
- GLib.markup_escape_text(status))
+
+ # italic
+ name += ('\n{}'.format(
+ GLib.markup_escape_text(status)))
icon_name = helpers.get_icon_name_to_show(contact, account)
# look if another resource has awaiting events
@@ -4688,6 +4683,9 @@ class RosterWindow:
gtkgui_helpers.set_unset_urgency_hint(self.window, nb_unread)
+ def _style_changed(self, *args):
+ self.change_roster_style(None)
+
def _change_style(self, model, path, titer, option):
if option is None or model[titer][Column.TYPE] == option:
# We changed style for this type of row
@@ -4751,18 +4749,18 @@ class RosterWindow:
return
theme = app.config.get('roster_theme')
if type_ == 'account':
- color = app.config.get_per('themes', theme, 'accounttextcolor')
- renderer.set_property('foreground', color or None)
- renderer.set_property('font',
- gtkgui_helpers.get_theme_font_for_option(theme, 'accountfont'))
+ color = app.css_config.get_value('.gajim-account-row', StyleAttr.COLOR)
+ renderer.set_property('foreground', color)
+ desc = app.css_config.get_font('.gajim-account-row')
+ renderer.set_property('font-desc', desc)
renderer.set_property('xpad', 0)
renderer.set_property('width', 3)
self._set_account_row_background_color(renderer)
elif type_ == 'group':
- color = app.config.get_per('themes', theme, 'grouptextcolor')
- renderer.set_property('foreground', color or None)
- renderer.set_property('font',
- gtkgui_helpers.get_theme_font_for_option(theme, 'groupfont'))
+ color = app.css_config.get_value('.gajim-group-row', StyleAttr.COLOR)
+ renderer.set_property('foreground', color)
+ desc = app.css_config.get_font('.gajim-group-row')
+ renderer.set_property('font-desc', desc)
parent_iter = model.iter_parent(titer)
if model[parent_iter][Column.TYPE] == 'group':
renderer.set_property('xpad', 8)
@@ -4781,19 +4779,16 @@ class RosterWindow:
ctrl = app.interface.minimized_controls[account].get(jid,
None)
if ctrl and ctrl.attention_flag:
- color = app.config.get_per('themes', theme,
- 'state_muc_directed_msg_color')
- renderer.set_property('foreground', 'red')
+ color = app.css_config.get_value(
+ '.state_muc_directed_msg_color', StyleAttr.COLOR)
+ renderer.set_property('foreground', color)
if not color:
- color = app.config.get_per('themes', theme,
- 'contacttextcolor')
- if color:
+ color = app.css_config.get_value('.gajim-contact-row', StyleAttr.COLOR)
renderer.set_property('foreground', color)
- else:
- renderer.set_property('foreground', None)
+
self._set_contact_row_background_color(renderer, jid, account)
- renderer.set_property('font',
- gtkgui_helpers.get_theme_font_for_option(theme, 'contactfont'))
+ desc = app.css_config.get_font('.gajim-contact-row')
+ renderer.set_property('font-desc', desc)
parent_iter = model.iter_parent(titer)
if model[parent_iter][Column.TYPE] == 'contact':
renderer.set_property('xpad', 16)
@@ -4886,25 +4881,25 @@ class RosterWindow:
def _set_account_row_background_color(self, renderer):
theme = app.config.get('roster_theme')
- color = app.config.get_per('themes', theme, 'accountbgcolor')
- renderer.set_property('cell-background', color or None)
+ color = app.css_config.get_value('.gajim-account-row', StyleAttr.BACKGROUND)
+ renderer.set_property('cell-background', color)
def _set_contact_row_background_color(self, renderer, jid, account):
theme = app.config.get('roster_theme')
if jid in app.newly_added[account]:
- renderer.set_property('cell-background', app.config.get(
- 'just_connected_bg_color'))
+ renderer.set_property('cell-background', app.css_config.get_value(
+ '.gajim-roster-connected', StyleAttr.BACKGROUND))
elif jid in app.to_be_removed[account]:
- renderer.set_property('cell-background', app.config.get(
- 'just_disconnected_bg_color'))
+ renderer.set_property('cell-background', app.css_config.get_value(
+ '.gajim-roster-disconnected', StyleAttr.BACKGROUND))
else:
- color = app.config.get_per('themes', theme, 'contactbgcolor')
- renderer.set_property('cell-background', color or None)
+ color = app.css_config.get_value('.gajim-contact-row', StyleAttr.BACKGROUND)
+ renderer.set_property('cell-background', color)
def _set_group_row_background_color(self, renderer):
theme = app.config.get('roster_theme')
- color = app.config.get_per('themes', theme, 'groupbgcolor')
- renderer.set_property('cell-background', color or None)
+ color = app.css_config.get_value('.gajim-group-row', 'background')
+ renderer.set_property('cell-background', color)
################################################################################
### Everything about building menus
@@ -5945,3 +5940,5 @@ class RosterWindow:
self._nec_decrypted_message_received)
app.ged.register_event_handler('blocking', ged.GUI1,
self._nec_blocking)
+ app.ged.register_event_handler('style-changed', ged.GUI1,
+ self._style_changed)