# 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 . import datetime from gi.repository import Gdk from gi.repository import GLib from gi.repository import Gtk from gi.repository import GObject from gajim.common.i18n import _ from gajim.common.i18n import Q_ from gajim.common.const import URIType from gajim.common.helpers import open_uri from gajim.common.helpers import parse_uri from gajim.common.structs import URI from gajim.gui.util import gtk_month from gajim.gui.util import python_month LABEL_DICT = { 'fn': _('Full Name'), 'n': _('Name'), 'bday': _('Birthday'), 'gender': _('Gender'), 'adr': Q_('?profile:Address'), 'tel': _('Phone No.'), 'email': _('Email'), 'impp': _('IM Address'), 'title': Q_('?profile:Title'), 'role': Q_('?profile:Role'), 'org': _('Organisation'), 'note': Q_('?profile:Note'), 'url': _('URL'), 'key': Q_('?profile:Public Encryption Key'), } FIELD_TOOLTIPS = { 'key': _('Your public key or authentication certificate') } ADR_FIELDS = ['street', 'ext', 'pobox', 'code', 'locality', 'region', 'country'] ADR_PLACEHOLDER_TEXT = { 'pobox': _('Post Office Box'), 'street': _('Street'), 'ext': _('Extended Address'), 'locality': _('City'), 'region': _('State'), 'code': _('Postal Code'), 'country': _('Country'), } DEFAULT_KWARGS = { 'fn': {'value': ''}, 'bday': {'value': '', 'value_type': 'date'}, 'gender': {'sex': '', 'identity': ''}, 'adr': {}, 'email': {'value': ''}, 'impp': {'value': ''}, 'tel': {'value': '', 'value_type': 'text'}, 'org': {'values': []}, 'title': {'value': ''}, 'role': {'value': ''}, 'url': {'value': ''}, 'key': {'value': '', 'value_type': 'text'}, 'note': {'value': ''}, } PROPERTIES_WITH_TYPE = [ 'adr', 'email', 'impp', 'tel', 'key', ] ORDER = [ 'fn', 'gender', 'bday', 'adr', 'email', 'impp', 'tel', 'org', 'title', 'role', 'url', 'key', 'note', ] SEX_VALUES = { 'M': _('Male'), 'F': _('Female'), 'O': Q_('?Gender:Other'), 'N': Q_('?Gender:None'), 'U': _('Unknown') } TYPE_VALUES = { '-': None, 'home': _('Home'), 'work': _('Work') } class VCardGrid(Gtk.Grid): def __init__(self, account): Gtk.Grid.__init__(self) self._callbacks = { 'fn': TextEntryProperty, 'bday': DateProperty, 'gender': GenderProperty, 'adr': AdrProperty, 'tel': TextEntryProperty, 'email': TextEntryProperty, 'impp': TextEntryProperty, 'title': TextEntryProperty, 'role': TextEntryProperty, 'org': TextEntryProperty, 'url': TextEntryProperty, 'key': KeyProperty, 'note': MultiLineProperty, } self.set_column_spacing(12) self.set_row_spacing(12) self.set_no_show_all(True) self.set_visible(True) self.set_halign(Gtk.Align.CENTER) self._account = account self._row_count = 0 self._vcard = None self._props = [] def set_editable(self, enabled): for prop in self._props: prop.set_editable(enabled) def set_vcard(self, vcard): self.clear() self._vcard = vcard for entry in ORDER: for prop in vcard.get_properties(): if entry != prop.name: continue self.add_property(prop) def get_vcard(self): return self._vcard def validate(self): for prop in list(self._props): base_prop = prop.get_base_property() if base_prop.is_empty: self.remove_property(prop) def add_new_property(self, name): kwargs = DEFAULT_KWARGS[name] prop = self._vcard.add_property(name, **kwargs) self.add_property(prop, editable=True) def add_property(self, prop, editable=False): prop_class = self._callbacks.get(prop.name) if prop_class is None: return prop_obj = prop_class(prop, self._account) prop_obj.set_editable(editable) prop_obj.add_to_grid(self, self._row_count) self._props.append(prop_obj) self._row_count += 1 def remove_property(self, prop): self.remove_row(prop.row_number) self._props.remove(prop) self._vcard.remove_property(prop.get_base_property()) def clear(self): self._vcard = None self._row_count = 0 for prop in list(self._props): self.remove_row(prop.row_number) self._props = [] def sort(self): self.set_vcard(self._vcard) class DescriptionLabel(Gtk.Label): def __init__(self, value): Gtk.Label.__init__(self, label=LABEL_DICT[value]) if value == 'adr': self.set_valign(Gtk.Align.START) else: self.set_valign(Gtk.Align.CENTER) self.get_style_context().add_class('dim-label') self.get_style_context().add_class('margin-right18') self.set_visible(True) self.set_xalign(1) self.set_tooltip_text(FIELD_TOOLTIPS.get(value, '')) class ValueLabel(Gtk.Label): def __init__(self, prop, account): Gtk.Label.__init__(self) self._prop = prop self._uri = None self._account = account self.set_selectable(True) self.set_xalign(0) self.set_max_width_chars(50) self.set_valign(Gtk.Align.CENTER) self.set_halign(Gtk.Align.START) self.connect('activate-link', self._on_activate_link) if prop.name == 'org': self.set_value(prop.values[0] if prop.values else '') else: self.set_value(prop.value) def set_value(self, value): if self._prop.name == 'email': self._uri = URI(type=URIType.MAIL, data=value) self.set_markup(value) elif self._prop.name in ('impp', 'tel'): self._uri = parse_uri(value) self.set_markup(value) else: self.set_text(value) def set_markup(self, text): if not text: self.set_text('') return super().set_markup('{}'.format( GLib.markup_escape_text(text), GLib.markup_escape_text(text))) def _on_activate_link(self, _label, _value): open_uri(self._uri, self._account) return Gdk.EVENT_STOP class SexLabel(Gtk.Label): def __init__(self, prop): Gtk.Label.__init__(self) self._prop = prop self.set_selectable(True) self.set_xalign(0) self.set_max_width_chars(50) self.set_valign(Gtk.Align.CENTER) self.set_halign(Gtk.Align.START) self.set_text(prop.sex) def set_text(self, value): if not value or value == '-': super().set_text('') else: super().set_text(SEX_VALUES[value]) class IdentityLabel(Gtk.Label): def __init__(self, prop): Gtk.Label.__init__(self) self._prop = prop self.set_selectable(True) self.set_xalign(0) self.set_max_width_chars(50) self.set_valign(Gtk.Align.CENTER) self.set_halign(Gtk.Align.START) self.set_text(prop.identity) def set_text(self, value): super().set_text('' if not value else value) class ValueEntry(Gtk.Entry): def __init__(self, prop): Gtk.Entry.__init__(self) self.set_valign(Gtk.Align.CENTER) self.set_max_width_chars(50) if prop.name == 'org': self.set_text(prop.values[0] if prop.values else '') else: self.set_text(prop.value) class AdrEntry(Gtk.Entry): def __init__(self, prop, type_): Gtk.Entry.__init__(self) self.set_valign(Gtk.Align.CENTER) self.set_max_width_chars(50) values = getattr(prop, type_) if not values: value = '' else: value = values[0] self.set_text(value) self.set_placeholder_text(ADR_PLACEHOLDER_TEXT.get(type_)) class IdentityEntry(Gtk.Entry): def __init__(self, prop): Gtk.Entry.__init__(self) self.set_valign(Gtk.Align.CENTER) self.set_max_width_chars(50) self.set_text('' if not prop.identity else prop.identity) class AdrBox(Gtk.Box): __gsignals__ = { 'field-changed': ( GObject.SignalFlags.RUN_LAST | GObject.SignalFlags.ACTION, None, # return value (str, str) # arguments )} def __init__(self, prop): Gtk.Box.__init__(self, orientation=Gtk.Orientation.VERTICAL, spacing=6) for field in ADR_FIELDS: entry = AdrEntry(prop, field) entry.connect('notify::text', self._on_text_changed, field) self.add(entry) self.show_all() def _on_text_changed(self, entry, _param, field): self.emit('field-changed', field, entry.get_text()) class AdrLabel(Gtk.Label): def __init__(self, prop, type_): Gtk.Label.__init__(self) self.set_selectable(True) self.set_xalign(0) self.set_max_width_chars(50) self.set_valign(Gtk.Align.CENTER) self.set_halign(Gtk.Align.START) values = getattr(prop, type_) if not values: value = '' else: value = values[0] self.set_text(value) def set_text(self, value): self.set_visible(bool(value)) super().set_text(value) class AdrBoxReadOnly(Gtk.Box): def __init__(self, prop): Gtk.Box.__init__(self, orientation=Gtk.Orientation.VERTICAL, spacing=6) self._labels = {} for field in ADR_FIELDS: label = AdrLabel(prop, field) self._labels[field] = label self.add(label) def set_field(self, field, value): self._labels[field].set_text(value) class ValueTextView(Gtk.TextView): def __init__(self, prop): Gtk.TextView.__init__(self) self.props.right_margin = 8 self.props.left_margin = 8 self.props.top_margin = 8 self.props.bottom_margin = 8 self.set_wrap_mode(Gtk.WrapMode.WORD_CHAR) self.set_hexpand(True) self.set_valign(Gtk.Align.FILL) self._prop = prop self.get_buffer().set_text(prop.value) self.get_buffer().connect('notify::text', self._on_text_changed) def get_text(self): start_iter, end_iter = self.get_buffer().get_bounds() return self.get_buffer().get_text(start_iter, end_iter, False) def _on_text_changed(self, _buffer, _param): self._prop.value = self.get_text() class TypeComboBox(Gtk.ComboBoxText): def __init__(self, parameters): Gtk.ComboBoxText.__init__(self) self.set_valign(Gtk.Align.CENTER) self._parameters = parameters self.append('-', '-') self.append('home', _('Home')) self.append('work', _('Work')) values = self._parameters.get_types() if 'home' in values: self.set_active_id('home') elif 'work' in values: self.set_active_id('work') else: self.set_active_id('-') self.connect('notify::active-id', self._on_active_id_changed) def _on_active_id_changed(self, _combobox, _param): type_ = self.get_active_id() if type_ == '-': self._parameters.remove_types(['work', 'home']) elif type_ == 'work': self._parameters.add_types(['work']) self._parameters.remove_types(['home']) elif type_ == 'home': self._parameters.add_types(['home']) self._parameters.remove_types(['work']) def get_text(self): type_value = self.get_active_id() if type_value == '-': return '' return self.get_active_text() class GenderComboBox(Gtk.ComboBoxText): def __init__(self, prop): Gtk.ComboBoxText.__init__(self) self.set_valign(Gtk.Align.CENTER) self.set_halign(Gtk.Align.START) self._prop = prop self.append('-', '-') for key, value in SEX_VALUES.items(): self.append(key, value) if not prop.sex: self.set_active_id('-') else: self.set_active_id(prop.sex) class RemoveButton(Gtk.Button): def __init__(self): Gtk.Button.__init__(self) self.set_valign(Gtk.Align.CENTER) self.set_halign(Gtk.Align.START) image = Gtk.Image.new_from_icon_name('user-trash-symbolic', Gtk.IconSize.MENU) self.set_image(image) self.set_no_show_all(True) class VCardProperty: def __init__(self, prop): self._prop = prop self._second_column = [] self._third_column = [] self._desc_label = DescriptionLabel(prop.name) self._remove_button = RemoveButton() self._remove_button.connect('clicked', self._on_remove_clicked) self._edit_widgets = [self._remove_button] self._read_widgets = [] if prop.name in PROPERTIES_WITH_TYPE: self._type_combobox = TypeComboBox(prop.parameters) self._type_combobox.connect('notify::active-id', self._on_type_changed) type_ = self._type_combobox.get_active_id() icon_name = self._get_icon_name(type_) self._type_image = Gtk.Image.new_from_icon_name( icon_name, Gtk.IconSize.MENU) self._type_image.set_tooltip_text(TYPE_VALUES[type_]) if prop.name == 'adr': self._type_image.set_valign(Gtk.Align.START) self._type_combobox.set_valign(Gtk.Align.START) self._edit_widgets.append(self._type_combobox) self._read_widgets.append(self._type_image) self._second_column.extend([self._type_combobox, self._type_image]) @staticmethod def _get_icon_name(type_): if type_ == 'home': return 'feather-home' if type_ == 'work': return 'feather-briefcase' return None def _on_type_changed(self, _combobox, _param): type_ = self._type_combobox.get_active_id() icon_name = self._get_icon_name(type_) self._type_image.set_from_icon_name(icon_name, Gtk.IconSize.MENU) self._type_image.set_tooltip_text(TYPE_VALUES[type_]) def _on_remove_clicked(self, button): button.get_parent().remove_property(self) @property def row_number(self): grid = self._desc_label.get_parent() return grid.child_get_property(self._desc_label, 'top-attach') def get_base_property(self): return self._prop def set_editable(self, enabled): for widget in self._edit_widgets: widget.set_visible(enabled) for widget in self._read_widgets: widget.set_visible(not enabled) def add_to_grid(self, grid, row_number): # child, left, top, width, height grid.attach(self._desc_label, 0, row_number, 1, 1) for widget in self._second_column: grid.attach(widget, 1, row_number, 1, 1) for widget in self._third_column: grid.attach(widget, 2, row_number, 1, 1) grid.attach(self._remove_button, 3, row_number, 1, 1) class TextEntryProperty(VCardProperty): def __init__(self, prop, account): VCardProperty.__init__(self, prop) self._value_entry = ValueEntry(prop) self._value_entry.connect('notify::text', self._on_text_changed) self._value_label = ValueLabel(prop, account) self._edit_widgets.append(self._value_entry) self._read_widgets.append(self._value_label) self._third_column = [self._value_entry, self._value_label] def _on_text_changed(self, entry, _param): text = entry.get_text() if self._prop.name == 'org': self._prop.values = [text] else: self._prop.value = text self._value_label.set_value(text) class MultiLineProperty(VCardProperty): def __init__(self, prop, _account): VCardProperty.__init__(self, prop) self._edit_text_view = ValueTextView(prop) self._edit_text_view.show() self._edit_scrolled = Gtk.ScrolledWindow() self._edit_scrolled.set_policy(Gtk.PolicyType.NEVER, Gtk.PolicyType.AUTOMATIC) self._edit_scrolled.add(self._edit_text_view) self._edit_scrolled.set_valign(Gtk.Align.CENTER) self._edit_scrolled.set_size_request(350, 100) self._edit_scrolled.get_style_context().add_class('profile-scrolled') self._read_text_view = ValueTextView(prop) self._read_text_view.set_sensitive(False) self._read_text_view.set_left_margin(0) self._read_text_view.show() self._read_scrolled = Gtk.ScrolledWindow() self._read_scrolled.set_policy(Gtk.PolicyType.NEVER, Gtk.PolicyType.AUTOMATIC) self._read_scrolled.add(self._read_text_view) self._read_scrolled.set_valign(Gtk.Align.CENTER) self._read_scrolled.set_size_request(350, 100) self._read_scrolled.get_style_context().add_class( 'profile-scrolled-read') self._edit_widgets.append(self._edit_scrolled) self._read_widgets.append(self._read_scrolled) self._third_column = [self._edit_scrolled, self._read_scrolled] class DateProperty(VCardProperty): def __init__(self, prop, account): VCardProperty.__init__(self, prop) self._box = Gtk.Box(spacing=6) self._value_entry = ValueEntry(prop) self._value_entry.set_placeholder_text(_('YYYY-MM-DD')) self._value_entry.connect('notify::text', self._on_text_changed) self._calendar_button = Gtk.MenuButton() image = Gtk.Image.new_from_icon_name( 'x-office-calendar-symbolic', Gtk.IconSize.BUTTON) self._calendar_button.set_image(image) self._calendar_button.connect( 'clicked', self._on_calendar_button_clicked) self._box.add(self._value_entry) self._box.add(self._calendar_button) self._box.show_all() self.calendar = Gtk.Calendar(year=1980, month=5, day=15) self.calendar.set_visible(True) self.calendar.connect( 'day-selected', self._on_calendar_day_selected) popover = Gtk.Popover() popover.add(self.calendar) self._calendar_button.set_popover(popover) self._value_label = ValueLabel(prop, account) self._edit_widgets.append(self._box) self._read_widgets.append(self._value_label) self._third_column = [self._box, self._value_label] def _on_text_changed(self, entry, _param): text = entry.get_text() self._prop.value = text self._value_label.set_value(text) def _on_calendar_button_clicked(self, _widget): birthday = self._value_entry.get_text() if not birthday: return try: date = datetime.datetime.strptime(birthday, '%Y-%m-%d') except ValueError: return month = gtk_month(date.month) self.calendar.select_month(month, date.year) self.calendar.select_day(date.day) def _on_calendar_day_selected(self, _widget): year, month, day = self.calendar.get_date() # Integers month = python_month(month) date_str = datetime.date(year, month, day).strftime('%Y-%m-%d') self._value_entry.set_text(date_str) class KeyProperty(VCardProperty): def __init__(self, prop, _account): VCardProperty.__init__(self, prop) self._value_text_view = ValueTextView(prop) self._value_text_view.show() self._scrolled_window = Gtk.ScrolledWindow() self._scrolled_window.set_policy(Gtk.PolicyType.NEVER, Gtk.PolicyType.AUTOMATIC) self._scrolled_window.add(self._value_text_view) self._scrolled_window.set_valign(Gtk.Align.CENTER) self._scrolled_window.set_size_request(350, 200) self._scrolled_window.get_style_context().add_class('profile-scrolled') self._copy_button = Gtk.Button.new_from_icon_name('edit-copy-symbolic', Gtk.IconSize.MENU) self._copy_button.connect('clicked', self._on_copy_clicked) self._copy_button.set_halign(Gtk.Align.START) self._copy_button.set_valign(Gtk.Align.CENTER) self._edit_widgets.append(self._scrolled_window) self._read_widgets.append(self._copy_button) self._third_column = [self._scrolled_window, self._copy_button] def _on_copy_clicked(self, _button): clipboard = Gtk.Clipboard.get(Gdk.SELECTION_CLIPBOARD) clipboard.set_text(self._value_text_view.get_text(), -1) class GenderProperty(VCardProperty): def __init__(self, prop, _account): VCardProperty.__init__(self, prop) value_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=12) self._value_combobox = GenderComboBox(prop) self._value_combobox.connect('notify::active-id', self._on_active_id_changed) self._value_combobox.show() self._value_entry = IdentityEntry(prop) self._value_entry.show() self._value_entry.connect('notify::text', self._on_text_changed) value_box.add(self._value_combobox) value_box.add(self._value_entry) label_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=12) self._identity_label = IdentityLabel(prop) self._identity_label.show() self._sex_label = SexLabel(prop) self._sex_label.show() label_box.add(self._sex_label) label_box.add(self._identity_label) self._edit_widgets.append(value_box) self._read_widgets.append(label_box) self._third_column = [value_box, label_box] def _on_text_changed(self, entry, _param): text = entry.get_text() self._prop.identity = text self._identity_label.set_text(text) def _on_active_id_changed(self, combobox, _param): sex = combobox.get_active_id() self._prop.sex = None if sex == '-' else sex self._sex_label.set_text(sex) class AdrProperty(VCardProperty): def __init__(self, prop, _account): VCardProperty.__init__(self, prop) self._entry_box = AdrBox(prop) self._entry_box.connect('field-changed', self._on_field_changed) self._read_box = AdrBoxReadOnly(prop) self._edit_widgets.append(self._entry_box) self._read_widgets.append(self._read_box) self._third_column = [self._entry_box, self._read_box] def _on_field_changed(self, _box, field, value): setattr(self._prop, field, [value]) self._read_box.set_field(field, value)