summaryrefslogtreecommitdiff
path: root/poezio/theming.py
diff options
context:
space:
mode:
authorEmmanuel Gil Peyrot <linkmauve@linkmauve.fr>2016-03-31 18:54:41 +0100
committerEmmanuel Gil Peyrot <linkmauve@linkmauve.fr>2016-06-11 20:49:43 +0100
commit332a5c2553db41de777473a1e1be9cd1522c9496 (patch)
tree3ee06a59f147ccc4009b35cccfbe2461bcd18310 /poezio/theming.py
parentcf44cf7cdec9fdb35caa372563d57e7045dc29dd (diff)
downloadpoezio-332a5c2553db41de777473a1e1be9cd1522c9496.tar.gz
poezio-332a5c2553db41de777473a1e1be9cd1522c9496.tar.bz2
poezio-332a5c2553db41de777473a1e1be9cd1522c9496.tar.xz
poezio-332a5c2553db41de777473a1e1be9cd1522c9496.zip
Move the src directory to poezio, for better cython compatibility.
Diffstat (limited to 'poezio/theming.py')
-rwxr-xr-xpoezio/theming.py534
1 files changed, 534 insertions, 0 deletions
diff --git a/poezio/theming.py b/poezio/theming.py
new file mode 100755
index 00000000..5d263741
--- /dev/null
+++ b/poezio/theming.py
@@ -0,0 +1,534 @@
+# Copyright 2010-2011 Florent Le Coz <louiz@louiz.org>
+#
+# This file is part of Poezio.
+#
+# Poezio is free software: you can redistribute it and/or modify
+# it under the terms of the zlib license. See the COPYING file.
+
+"""
+Define the variables (colors and some other stuff) that are
+used when drawing the interface.
+
+Colors are numbers from -1 to 7 (if only 8 colors are supported) or -1 to 255
+if 256 colors are available.
+If only 8 colors are available, all colors > 8 are converted using the
+table_256_to_16 dict.
+
+XHTML-IM colors are converted to -1 -> 255 colors if available, or directly to
+-1 -> 8 if we are in 8-color-mode.
+
+A pair_color is a background-foreground pair. All possible pairs are not created
+at startup, because that would create 256*256 pairs, and almost all of them
+would never be used.
+
+A theme should define color tuples, like ``(200, -1)``, and when they are to
+be used by poezio's interface, they will be created once, and kept in a list for
+later usage.
+A color tuple is of the form ``(foreground, background, optional)``
+A color of -1 means the default color. So if you do not want to have
+a background color, use ``(x, -1)``.
+The optional third value of the tuple defines additional information. It
+is a string and can contain one or more of these characters:
+
+- ``b``: bold
+- ``u``: underlined
+- ``x``: blink
+
+For example, ``(200, 208, 'bu')`` is bold, underlined and pink foreground on
+orange background.
+
+A theme file is a python file containing one object named 'theme', which is an
+instance of a class (derived from the Theme class) defined in that same file.
+For example, in pinkytheme.py:
+
+.. code-block:: python
+
+ import theming
+ class PinkyTheme(theming.Theme):
+ COLOR_NORMAL_TEXT = (200, -1)
+
+ theme = PinkyTheme()
+
+if the command '/theme pinkytheme' is issued, we import the pinkytheme.py file
+and set the global variable 'theme' to pinkytheme.theme.
+
+And in poezio's code we just use ``theme.COLOR_NORMAL_TEXT`` etc
+
+Since a theme inherites from the Theme class (defined here), if a color is not defined in a
+theme file, the color is the default one.
+
+Some values in that class are a list of color tuple.
+For example ``[(1, -1), (2, -1), (3, -1)]``
+Such a list SHOULD contain at least one color tuple.
+It is used for example to define color gradient, etc.
+"""
+
+import logging
+log = logging.getLogger(__name__)
+
+from config import config
+
+import curses
+import os
+from os import path
+
+from importlib import machinery
+finder = machinery.PathFinder()
+
+class Theme(object):
+ """
+ The theme class, from which all themes should inherit.
+ All of the following values can be replaced in subclasses, in
+ order to create a new theme.
+
+ Do not edit this file if you want to change the theme to suit your
+ needs. Create a new theme and share it if you think it can be useful
+ for others.
+ """
+ @classmethod
+ def color_role(cls, role):
+ role_mapping = {
+ 'moderator': cls.COLOR_USER_MODERATOR,
+ 'participant': cls.COLOR_USER_PARTICIPANT,
+ 'visitor': cls.COLOR_USER_VISITOR,
+ 'none': cls.COLOR_USER_NONE,
+ '': cls.COLOR_USER_NONE
+ }
+ return role_mapping.get(role, cls.COLOR_USER_NONE)
+
+ @classmethod
+ def char_affiliation(cls, affiliation):
+ affiliation_mapping = {
+ 'owner': cls.CHAR_AFFILIATION_OWNER,
+ 'admin': cls.CHAR_AFFILIATION_ADMIN,
+ 'member': cls.CHAR_AFFILIATION_MEMBER,
+ 'none': cls.CHAR_AFFILIATION_NONE
+ }
+ return affiliation_mapping.get(affiliation, cls.CHAR_AFFILIATION_NONE)
+
+ @classmethod
+ def color_show(cls, show):
+ show_mapping = {
+ 'xa': cls.COLOR_STATUS_XA,
+ 'none': cls.COLOR_STATUS_NONE,
+ 'dnd': cls.COLOR_STATUS_DND,
+ 'away': cls.COLOR_STATUS_AWAY,
+ 'chat': cls.COLOR_STATUS_CHAT,
+ '': cls.COLOR_STATUS_ONLINE,
+ 'available': cls.COLOR_STATUS_ONLINE,
+ 'unavailable': cls.COLOR_STATUS_UNAVAILABLE,
+ }
+ return show_mapping.get(show, cls.COLOR_STATUS_NONE)
+
+ @classmethod
+ def char_subscription(cls, sub, keep='incomplete'):
+ sub_mapping = {
+ 'from': cls.CHAR_ROSTER_FROM,
+ 'both': cls.CHAR_ROSTER_BOTH,
+ 'none': cls.CHAR_ROSTER_NONE,
+ 'to': cls.CHAR_ROSTER_TO,
+ }
+ if keep == 'incomplete' and sub == 'both':
+ return ''
+ if keep in ('both', 'none', 'to', 'from'):
+ return sub_mapping[sub] if sub == keep else ''
+ return sub_mapping.get(sub, '')
+
+ # Message text color
+ COLOR_NORMAL_TEXT = (-1, -1)
+ COLOR_INFORMATION_TEXT = (5, -1) # TODO
+ COLOR_WARNING_TEXT = (1, -1)
+
+ # Color of the commands in the help message
+ COLOR_HELP_COMMANDS = (208, -1)
+
+ # "reverse" is a special value, available only for this option. It just
+ # takes the nick colors and reverses it. A theme can still specify a
+ # fixed color if need be.
+ COLOR_HIGHLIGHT_NICK = "reverse"
+
+ # Color of the participant JID in a MUC
+ COLOR_MUC_JID = (4, -1)
+
+ # User list color
+ COLOR_USER_VISITOR = (239, -1)
+ COLOR_USER_PARTICIPANT = (4, -1)
+ COLOR_USER_NONE = (0, -1)
+ COLOR_USER_MODERATOR = (1, -1)
+
+ # nickname colors
+ COLOR_REMOTE_USER = (5, -1)
+
+ # The character printed in color (COLOR_STATUS_*) before the nickname
+ # in the user list
+ CHAR_STATUS = '|'
+
+ # The characters used for the chatstates in the user list
+ # in a MUC
+ CHAR_CHATSTATE_ACTIVE = 'A'
+ CHAR_CHATSTATE_COMPOSING = 'X'
+ CHAR_CHATSTATE_PAUSED = 'p'
+
+ # These characters are used for the affiliation in the user list
+ # in a MUC
+ CHAR_AFFILIATION_OWNER = '~'
+ CHAR_AFFILIATION_ADMIN = '&'
+ CHAR_AFFILIATION_MEMBER = '+'
+ CHAR_AFFILIATION_NONE = '-'
+
+
+ # XML Tab
+ CHAR_XML_IN = 'IN '
+ CHAR_XML_OUT = 'OUT'
+ COLOR_XML_IN = (1, -1)
+ COLOR_XML_OUT = (2, -1)
+
+ # Color for the /me message
+ COLOR_ME_MESSAGE = (6, -1)
+
+ # Color for the number of revisions of a message
+ COLOR_REVISIONS_MESSAGE = (3, -1, 'b')
+
+ # Color for various important text. For example the "?" before JIDs in
+ # the roster that require an user action.
+ COLOR_IMPORTANT_TEXT = (3, 5, 'b')
+
+ # Separators
+ COLOR_VERTICAL_SEPARATOR = (4, -1)
+ COLOR_NEW_TEXT_SEPARATOR = (2, -1)
+ COLOR_MORE_INDICATOR = (6, 4)
+
+ # Time
+ CHAR_TIME_LEFT = ''
+ CHAR_TIME_RIGHT = ''
+ COLOR_TIME_STRING = (-1, -1)
+
+ # Tabs
+ COLOR_TAB_NORMAL = (7, 4)
+ COLOR_TAB_NONEMPTY = (7, 4)
+ COLOR_TAB_SCROLLED = (5, 4)
+ COLOR_TAB_JOINED = (82, 4)
+ COLOR_TAB_CURRENT = (7, 6)
+ COLOR_TAB_COMPOSING = (7, 5)
+ COLOR_TAB_NEW_MESSAGE = (7, 5)
+ COLOR_TAB_HIGHLIGHT = (7, 3)
+ COLOR_TAB_PRIVATE = (7, 2)
+ COLOR_TAB_ATTENTION = (7, 1)
+ COLOR_TAB_DISCONNECTED = (7, 8)
+
+ COLOR_VERTICAL_TAB_NORMAL = (4, -1)
+ COLOR_VERTICAL_TAB_NONEMPTY = (4, -1)
+ COLOR_VERTICAL_TAB_JOINED = (82, -1)
+ COLOR_VERTICAL_TAB_SCROLLED = (66, -1)
+ COLOR_VERTICAL_TAB_CURRENT = (7, 4)
+ COLOR_VERTICAL_TAB_NEW_MESSAGE = (5, -1)
+ COLOR_VERTICAL_TAB_COMPOSING = (5, -1)
+ COLOR_VERTICAL_TAB_HIGHLIGHT = (3, -1)
+ COLOR_VERTICAL_TAB_PRIVATE = (2, -1)
+ COLOR_VERTICAL_TAB_ATTENTION = (1, -1)
+ COLOR_VERTICAL_TAB_DISCONNECTED = (8, -1)
+
+ # Nickname colors
+ # A list of colors randomly attributed to nicks in MUCs
+ # Setting more colors makes it harder to have two nicks with the same color,
+ # avoiding confusions.
+ LIST_COLOR_NICKNAMES = [
+ (1, -1), (2, -1), (3, -1), (4, -1), (5, -1), (6, -1), (9, -1),
+ (10, -1), (11, -1), (12, -1), (13, -1), (14, -1), (19, -1),
+ (20, -1), (21, -1), (22, -1), (23, -1), (24, -1), (25, -1),
+ (26, -1), (27, -1), (28, -1), (29, -1), (30, -1), (31, -1),
+ (32, -1), (33, -1), (34, -1), (35, -1), (36, -1), (37, -1),
+ (38, -1), (39, -1), (40, -1), (41, -1), (42, -1), (43, -1),
+ (44, -1), (45, -1), (46, -1), (47, -1), (48, -1), (49, -1),
+ (50, -1), (51, -1), (54, -1), (55, -1), (56, -1), (57, -1),
+ (58, -1), (60, -1), (61, -1), (62, -1), (63, -1), (64, -1),
+ (65, -1), (66, -1), (67, -1), (68, -1), (69, -1), (70, -1),
+ (71, -1), (72, -1), (73, -1), (74, -1), (75, -1), (76, -1),
+ (77, -1), (78, -1), (79, -1), (80, -1), (81, -1), (82, -1),
+ (83, -1), (84, -1), (85, -1), (86, -1), (87, -1), (88, -1),
+ (89, -1), (90, -1), (91, -1), (92, -1), (93, -1), (94, -1),
+ (95, -1), (96, -1), (97, -1), (98, -1), (99, -1), (100, -1),
+ (101, -1), (103, -1), (104, -1), (105, -1), (106, -1), (107, -1),
+ (108, -1), (109, -1), (110, -1), (111, -1), (112, -1), (113, -1),
+ (114, -1), (115, -1), (116, -1), (117, -1), (118, -1), (119, -1),
+ (120, -1), (121, -1), (122, -1), (123, -1), (124, -1), (125, -1),
+ (126, -1), (127, -1), (128, -1), (129, -1), (130, -1), (131, -1),
+ (132, -1), (133, -1), (134, -1), (135, -1), (136, -1), (137, -1),
+ (138, -1), (139, -1), (140, -1), (141, -1), (142, -1), (143, -1),
+ (144, -1), (145, -1), (146, -1), (147, -1), (148, -1), (149, -1),
+ (150, -1), (151, -1), (152, -1), (153, -1), (154, -1), (155, -1),
+ (156, -1), (157, -1), (158, -1), (159, -1), (160, -1), (161, -1),
+ (162, -1), (163, -1), (164, -1), (165, -1), (166, -1), (167, -1),
+ (168, -1), (169, -1), (170, -1), (171, -1), (172, -1), (173, -1),
+ (174, -1), (175, -1), (176, -1), (177, -1), (178, -1), (179, -1),
+ (180, -1), (181, -1), (182, -1), (183, -1), (184, -1), (185, -1),
+ (186, -1), (187, -1), (188, -1), (189, -1), (190, -1), (191, -1),
+ (192, -1), (193, -1), (196, -1), (197, -1), (198, -1), (199, -1),
+ (200, -1), (201, -1), (202, -1), (203, -1), (204, -1), (205, -1),
+ (206, -1), (207, -1), (208, -1), (209, -1), (210, -1), (211, -1),
+ (212, -1), (213, -1), (214, -1), (215, -1), (216, -1), (217, -1),
+ (218, -1), (219, -1), (220, -1), (221, -1), (222, -1), (223, -1),
+ (224, -1), (225, -1), (226, -1), (227, -1)]
+
+ # This is your own nickname
+ COLOR_OWN_NICK = (254, -1)
+
+ COLOR_LOG_MSG = (5, -1)
+ # This is for in-tab error messages
+ COLOR_ERROR_MSG = (9, 7, 'b')
+ # Status color
+ COLOR_STATUS_XA = (16, 90)
+ COLOR_STATUS_NONE = (16, 4)
+ COLOR_STATUS_DND = (16, 1)
+ COLOR_STATUS_AWAY = (16, 3)
+ COLOR_STATUS_CHAT = (16, 2)
+ COLOR_STATUS_UNAVAILABLE = (-1, 247)
+ COLOR_STATUS_ONLINE = (16, 4)
+
+ # Bars
+ COLOR_WARNING_PROMPT = (16, 1, 'b')
+ COLOR_INFORMATION_BAR = (7, 4)
+ COLOR_TOPIC_BAR = (7, 4)
+ COLOR_SCROLLABLE_NUMBER = (220, 4, 'b')
+ COLOR_SELECTED_ROW = (-1, 33)
+ COLOR_PRIVATE_NAME = (-1, 4)
+ COLOR_CONVERSATION_NAME = (2, 4)
+ COLOR_CONVERSATION_RESOURCE = (121, 4)
+ COLOR_GROUPCHAT_NAME = (7, 4)
+ COLOR_COLUMN_HEADER = (36, 4)
+ COLOR_COLUMN_HEADER_SEL = (4, 36)
+
+ # Strings for special messages (like join, quit, nick change, etc)
+ # Special messages
+ CHAR_JOIN = '--->'
+ CHAR_QUIT = '<---'
+ CHAR_KICK = '-!-'
+ CHAR_NEW_TEXT_SEPARATOR = '- '
+ CHAR_OK = '✔'
+ CHAR_ERROR = '✖'
+ CHAR_EMPTY = ' '
+ CHAR_ACK_RECEIVED = CHAR_OK
+ CHAR_NACK = CHAR_ERROR
+ CHAR_COLUMN_ASC = ' ▲'
+ CHAR_COLUMN_DESC = ' ▼'
+ CHAR_ROSTER_ERROR = CHAR_ERROR
+ CHAR_ROSTER_TUNE = '♪'
+ CHAR_ROSTER_ASKED = '?'
+ CHAR_ROSTER_ACTIVITY = 'A'
+ CHAR_ROSTER_MOOD = 'M'
+ CHAR_ROSTER_GAMING = 'G'
+ CHAR_ROSTER_FROM = '←'
+ CHAR_ROSTER_BOTH = '↔'
+ CHAR_ROSTER_TO = '→'
+ CHAR_ROSTER_NONE = '⇹'
+
+ COLOR_CHAR_ACK = (2, -1)
+ COLOR_CHAR_NACK = (1, -1)
+
+ COLOR_ROSTER_GAMING = (6, -1)
+ COLOR_ROSTER_MOOD = (2, -1)
+ COLOR_ROSTER_ACTIVITY = (3, -1)
+ COLOR_ROSTER_TUNE = (6, -1)
+ COLOR_ROSTER_ERROR = (1, -1)
+ COLOR_ROSTER_SUBSCRIPTION = (-1, -1)
+
+ COLOR_JOIN_CHAR = (4, -1)
+ COLOR_QUIT_CHAR = (1, -1)
+ COLOR_KICK_CHAR = (1, -1)
+
+ # Vertical tab list color
+ COLOR_VERTICAL_TAB_NUMBER = (34, -1)
+
+ # Info messages color (the part before the ">")
+ INFO_COLORS = {
+ 'info': (5, -1),
+ 'error': (16, 1),
+ 'warning': (1, -1),
+ 'roster': (2, -1),
+ 'help': (10, -1),
+ 'headline': (11, -1, 'b'),
+ 'tune': (6, -1),
+ 'gaming': (6, -1),
+ 'mood': (2, -1),
+ 'activity': (3, -1),
+ 'default': (7, -1),
+ }
+
+# This is the default theme object, used if no theme is defined in the conf
+theme = Theme()
+
+# a dict "color tuple -> color_pair"
+# Each time we use a color tuple, we check if it has already been used.
+# If not we create a new color_pair and keep it in that dict, to use it
+# the next time.
+curses_colors_dict = {}
+
+table_256_to_16 = [
+ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15,
+ 0, 4, 4, 4, 12, 12, 2, 6, 4, 4, 12, 12, 2, 2, 6, 4,
+ 12, 12, 2, 2, 2, 6, 12, 12, 10, 10, 10, 10, 14, 12, 10, 10,
+ 10, 10, 10, 14, 1, 5, 4, 4, 12, 12, 3, 8, 4, 4, 12, 12,
+ 2, 2, 6, 4, 12, 12, 2, 2, 2, 6, 12, 12, 10, 10, 10, 10,
+ 14, 12, 10, 10, 10, 10, 10, 14, 1, 1, 5, 4, 12, 12, 1, 1,
+ 5, 4, 12, 12, 3, 3, 8, 4, 12, 12, 2, 2, 2, 6, 12, 12,
+ 10, 10, 10, 10, 14, 12, 10, 10, 10, 10, 10, 14, 1, 1, 1, 5,
+ 12, 12, 1, 1, 1, 5, 12, 12, 1, 1, 1, 5, 12, 12, 3, 3,
+ 3, 7, 12, 12, 10, 10, 10, 10, 14, 12, 10, 10, 10, 10, 10, 14,
+ 9, 9, 9, 9, 13, 12, 9, 9, 9, 9, 13, 12, 9, 9, 9, 9,
+ 13, 12, 9, 9, 9, 9, 13, 12, 11, 11, 11, 11, 7, 12, 10, 10,
+ 10, 10, 10, 14, 9, 9, 9, 9, 9, 13, 9, 9, 9, 9, 9, 13,
+ 9, 9, 9, 9, 9, 13, 9, 9, 9, 9, 9, 13, 9, 9, 9, 9,
+ 9, 13, 11, 11, 11, 11, 11, 15, 0, 0, 0, 0, 0, 0, 8, 8,
+ 8, 8, 8, 8, 7, 7, 7, 7, 7, 7, 15, 15, 15, 15, 15, 15
+]
+
+load_path = []
+
+def color_256_to_16(color):
+ if color == -1:
+ return color
+ return table_256_to_16[color]
+
+def dump_tuple(tup):
+ """
+ Dump a tuple to a string of fg,bg,attr (optional)
+ """
+ return ','.join(str(i) for i in tup)
+
+def read_tuple(_str):
+ """
+ Read a tuple dumped with dump_tumple
+ """
+ attrs = _str.split(',')
+ char = attrs[2] if len(attrs) > 2 else None
+ return (int(attrs[0]), int(attrs[1])), char
+
+def to_curses_attr(color_tuple):
+ """
+ Takes a color tuple (as defined at the top of this file) and
+ returns a valid curses attr that can be passed directly to attron() or attroff()
+ """
+ # extract the color from that tuple
+ if len(color_tuple) == 3:
+ colors = (color_tuple[0], color_tuple[1])
+ else:
+ colors = color_tuple
+
+ bold = False
+ if curses.COLORS != 256:
+ # We are not in a term supporting 256 colors, so we convert
+ # colors to numbers between -1 and 8
+ colors = (color_256_to_16(colors[0]), color_256_to_16(colors[1]))
+ if colors[0] >= 8:
+ colors = (colors[0] - 8, colors[1])
+ bold = True
+ if colors[1] >= 8:
+ colors = (colors[0], colors[1] - 8)
+
+ # check if we already used these colors
+ try:
+ pair = curses_colors_dict[colors]
+ except KeyError:
+ pair = len(curses_colors_dict) + 1
+ curses.init_pair(pair, colors[0], colors[1])
+ curses_colors_dict[colors] = pair
+ curses_pair = curses.color_pair(pair)
+ if len(color_tuple) == 3:
+ additional_val = color_tuple[2]
+ if 'b' in additional_val or bold is True:
+ curses_pair = curses_pair | curses.A_BOLD
+ if 'u' in additional_val:
+ curses_pair = curses_pair | curses.A_UNDERLINE
+ if 'a' in additional_val:
+ curses_pair = curses_pair | curses.A_BLINK
+ return curses_pair
+
+def get_theme():
+ """
+ Returns the current theme
+ """
+ return theme
+
+def update_themes_dir(option=None, value=None):
+ global load_path
+ load_path = []
+
+ # import from the git sources
+ default_dir = path.join(
+ path.dirname(path.dirname(__file__)),
+ 'data/themes')
+ if path.exists(default_dir):
+ load_path.append(default_dir)
+
+ # import from the user-defined prefs
+ themes_dir = path.expanduser(
+ value or
+ config.get('themes_dir') or
+ path.join(os.environ.get('XDG_DATA_HOME') or
+ path.join(os.environ.get('HOME'), '.local', 'share'),
+ 'poezio', 'themes')
+ )
+ try:
+ os.makedirs(themes_dir)
+ except OSError as e:
+ if e.errno != 17:
+ log.error('Unable to create the themes dir (%s)', themes_dir)
+ else:
+ load_path.append(themes_dir)
+ else:
+ load_path.append(themes_dir)
+
+ # system-wide import
+ try:
+ import poezio_themes
+ except:
+ pass
+ else:
+ if poezio_themes.__path__:
+ load_path.append(list(poezio_themes.__path__)[0])
+
+ log.debug('Theme load path: %s', load_path)
+
+def reload_theme():
+ theme_name = config.get('theme')
+ global theme
+ if theme_name == 'default' or not theme_name.strip():
+ theme = Theme()
+ return
+ new_theme = None
+ exc = None
+ try:
+ loader = finder.find_module(theme_name, load_path)
+ if not loader:
+ return 'Failed to load the theme %s' % theme_name
+ new_theme = loader.load_module()
+ except Exception as e:
+ log.error('Failed to load the theme %s', theme_name, exc_info=True)
+ exc = e
+
+ if not new_theme:
+ return 'Failed to load theme: %s' % exc
+
+ if hasattr(new_theme, 'theme'):
+ theme = new_theme.theme
+ else:
+ return 'No theme present in the theme file'
+
+if __name__ == '__main__':
+ # Display some nice text with nice colors
+ s = curses.initscr()
+ curses.start_color()
+ curses.use_default_colors()
+ s.addstr('%s colors detected\n\n' % curses.COLORS, to_curses_attr((3, -1)))
+ for i in range(curses.COLORS):
+ s.addstr('%s ' % i, to_curses_attr((i, -1)))
+ s.addstr('\n')
+ s.refresh()
+ try:
+ s.getkey()
+ except KeyboardInterrupt:
+ pass
+ finally:
+ curses.endwin()
+ print()
+