Rewrite GroupchatConfig dialog

- Complete rewrite of the dialog
- Use new DataFormWidget
This commit is contained in:
Philipp Hörist 2018-11-02 22:24:28 +01:00
parent e615a8e2e3
commit 61a791d67c
7 changed files with 892 additions and 242 deletions

View File

@ -193,6 +193,16 @@ class SyncThreshold(IntEnum):
return str(self.value)
class MUCUser(IntEnum):
JID = 0
NICK = 1
REASON = 1
NICK_OR_REASON = 1
ROLE = 2
AFFILIATION = 3
AFFILIATION_TEXT = 4
ACTIVITIES = {
'doing_chores': {
'category': _('Doing Chores'),

View File

@ -17,6 +17,7 @@
import time
import logging
import weakref
import nbxmpp
@ -187,28 +188,40 @@ class MUC:
item = iq.setQuery()
for jid in users_dict:
affiliation = users_dict[jid].get('affiliation')
reason = users_dict[jid].get('reason') or None
reason = users_dict[jid].get('reason')
nick = users_dict[jid].get('nick')
item_tag = item.addChild('item', {'jid': jid,
'affiliation': affiliation})
if reason is not None:
item_tag.setTagData('reason', reason)
if nick is not None:
item_tag.setAttr('nick', nick)
log.info('Set affiliation for %s: %s', room_jid, users_dict)
self._con.connection.SendAndCallForResponse(
iq, self._default_response, {})
def get_affiliation(self, room_jid, affiliation):
def get_affiliation(self, room_jid, affiliation, success_cb, error_cb):
if not app.account_is_connected(self._account):
return
iq = nbxmpp.Iq(typ='get', to=room_jid, queryNS=nbxmpp.NS_MUC_ADMIN)
item = iq.setQuery().setTag('item')
item.setAttr('affiliation', affiliation)
log.info('Get affiliation %s for %s', affiliation, room_jid)
self._con.connection.SendAndCallForResponse(
iq, self._affiliation_received)
def _affiliation_received(self, stanza):
weak_success_cb = weakref.WeakMethod(success_cb)
weak_error_cb = weakref.WeakMethod(error_cb)
self._con.connection.SendAndCallForResponse(
iq, self._affiliation_received, {'affiliation': affiliation,
'success_cb': weak_success_cb,
'error_cb': weak_error_cb})
def _affiliation_received(self, _con, stanza, affiliation,
success_cb, error_cb):
if not nbxmpp.isResultNode(stanza):
log.info('Error: %s', stanza.getError())
if error_cb() is not None:
error_cb()(affiliation, stanza.getError())
return
room_jid = stanza.getFrom().getStripped()
@ -222,8 +235,8 @@ class MUC:
log.warning('Invalid JID: %s, ignoring it',
item.getAttr('jid'))
continue
affiliation = item.getAttr('affiliation')
users_dict[jid] = {'affiliation': affiliation}
users_dict[jid] = {}
if item.has_attr('nick'):
users_dict[jid]['nick'] = item.getAttr('nick')
if item.has_attr('role'):
@ -231,9 +244,12 @@ class MUC:
reason = item.getTagData('reason')
if reason:
users_dict[jid]['reason'] = reason
log.info('Affiliations received from %s: %s', room_jid, users_dict)
app.nec.push_incoming_event(MucAdminReceivedEvent(
None, conn=self._con, room_jid=room_jid, users_dict=users_dict))
log.info('%s affiliations received from %s: %s',
affiliation, room_jid, users_dict)
if success_cb() is not None:
success_cb()(self._account, room_jid, affiliation, users_dict)
def set_role(self, room_jid, nick, role, reason=''):
if not app.account_is_connected(self._account):
@ -423,10 +439,6 @@ class GcDeclineReceived(NetworkIncomingEvent):
name = 'gc-decline-received'
class MucAdminReceivedEvent(NetworkIncomingEvent):
name = 'muc-admin-received'
class MucOwnerReceivedEvent(NetworkIncomingEvent):
name = 'muc-owner-received'

View File

@ -0,0 +1,493 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- Generated with glade 3.22.1 -->
<interface>
<requires lib="gtk+" version="3.20"/>
<object class="GtkListStore" id="affiliation_store">
<columns>
<!-- column-name jabberid -->
<column type="gchararray"/>
<!-- column-name nickname -->
<column type="gchararray"/>
<!-- column-name role -->
<column type="gchararray"/>
<!-- column-name affiliation -->
<column type="gchararray"/>
<!-- column-name affiliation-text -->
<column type="gchararray"/>
<!-- column-name editable-affiliation -->
<column type="gboolean"/>
<!-- column-name editable-jid -->
<column type="gboolean"/>
</columns>
</object>
<object class="GtkListStore" id="combo_store">
<columns>
<!-- column-name affiliation -->
<column type="gchararray"/>
<!-- column-name affiliation-text -->
<column type="gchararray"/>
</columns>
<data>
<row>
<col id="0" translatable="yes">Owner</col>
<col id="1">owner</col>
</row>
<row>
<col id="0" translatable="yes">Admin</col>
<col id="1">admin</col>
</row>
<row>
<col id="0" translatable="yes">Member</col>
<col id="1">member</col>
</row>
</data>
</object>
<object class="GtkPopover" id="info_popover">
<property name="can_focus">False</property>
<child>
<object class="GtkLabel">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="margin_left">12</property>
<property name="margin_right">12</property>
<property name="margin_top">12</property>
<property name="margin_bottom">12</property>
<property name="label" translatable="yes">&lt;b&gt;Jabber-ID&lt;/b&gt;
&amp;lt;user@domain/resource&amp;gt; (only that resource matches)
&amp;lt;user@domain&amp;gt; (any resource matches)
&amp;lt;domain/resource&amp;gt; (only that resource matches)
&amp;lt;domain&amp;gt; (the domain itself matches, as does any user@domain or domain/resource)
</property>
<property name="use_markup">True</property>
</object>
</child>
</object>
<object class="GtkListStore" id="outcast_store">
<columns>
<!-- column-name jabberid -->
<column type="gchararray"/>
<!-- column-name reason -->
<column type="gchararray"/>
<!-- column-name dummy -->
<column type="gchararray"/>
<!-- column-name affiliation -->
<column type="gchararray"/>
<!-- column-name dummy1 -->
<column type="gchararray"/>
<!-- column-name dummy2 -->
<column type="gboolean"/>
<!-- column-name editable-jid -->
<column type="gboolean"/>
</columns>
</object>
<object class="GtkGrid" id="grid">
<property name="name">GroupchatConfig</property>
<property name="width_request">700</property>
<property name="height_request">500</property>
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="row_spacing">12</property>
<child>
<object class="GtkBox">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="hexpand">False</property>
<child>
<object class="GtkBox" id="treeview_buttonbox">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="valign">start</property>
<property name="margin_left">12</property>
<property name="vexpand">False</property>
<property name="spacing">6</property>
<child>
<object class="GtkMenuButton">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">True</property>
<property name="popover">info_popover</property>
<child>
<object class="GtkImage">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="icon_name">dialog-information-symbolic</property>
</object>
</child>
<style>
<class name="suggested-action"/>
</style>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">False</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkButton" id="add_button">
<property name="visible">True</property>
<property name="sensitive">False</property>
<property name="can_focus">True</property>
<property name="receives_default">True</property>
<property name="tooltip_text" translatable="yes">Only Admins and Owners can modify the affiliation</property>
<property name="halign">start</property>
<signal name="clicked" handler="_on_add" swapped="no"/>
<child>
<object class="GtkImage">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="icon_name">list-add-symbolic</property>
</object>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">False</property>
<property name="position">1</property>
</packing>
</child>
<child>
<object class="GtkButton" id="remove_button">
<property name="label" translatable="yes">Remove</property>
<property name="visible">True</property>
<property name="sensitive">False</property>
<property name="can_focus">True</property>
<property name="receives_default">True</property>
<property name="tooltip_text" translatable="yes">Only Admins and Owners can modify the affiliation</property>
<signal name="clicked" handler="_on_remove" swapped="no"/>
<style>
<class name="destructive-action"/>
</style>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">False</property>
<property name="position">2</property>
</packing>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">False</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkButtonBox" id="close_ok_button_box">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="spacing">6</property>
<property name="layout_style">end</property>
<child>
<object class="GtkButton">
<property name="label">gtk-cancel</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">True</property>
<property name="use_stock">True</property>
<signal name="clicked" handler="_on_cancel" swapped="no"/>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">False</property>
<property name="pack_type">end</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkButton">
<property name="label">gtk-ok</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">True</property>
<property name="use_stock">True</property>
<signal name="clicked" handler="_on_ok" swapped="no"/>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">False</property>
<property name="pack_type">end</property>
<property name="position">1</property>
</packing>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">False</property>
<property name="pack_type">end</property>
<property name="position">1</property>
</packing>
</child>
</object>
<packing>
<property name="left_attach">1</property>
<property name="top_attach">1</property>
</packing>
</child>
<child>
<object class="GtkStack" id="stack">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="hexpand">True</property>
<property name="vexpand">True</property>
<property name="transition_duration">300</property>
<property name="transition_type">crossfade</property>
<signal name="notify::visible-child-name" handler="_on_switch_page" swapped="no"/>
<child>
<object class="GtkGrid" id="config_grid">
<property name="visible">True</property>
<property name="can_focus">False</property>
<child>
<placeholder/>
</child>
<child>
<placeholder/>
</child>
<child>
<placeholder/>
</child>
<child>
<placeholder/>
</child>
<child>
<placeholder/>
</child>
<child>
<placeholder/>
</child>
<child>
<placeholder/>
</child>
<child>
<placeholder/>
</child>
<child>
<placeholder/>
</child>
</object>
<packing>
<property name="name">config</property>
<property name="title" translatable="yes">Configuration</property>
</packing>
</child>
<child>
<object class="GtkGrid">
<property name="visible">True</property>
<property name="can_focus">False</property>
<child>
<object class="GtkScrolledWindow">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="hexpand">True</property>
<property name="vexpand">True</property>
<property name="hscrollbar_policy">never</property>
<property name="shadow_type">in</property>
<child>
<object class="GtkTreeView" id="affiliation_treeview">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="model">affiliation_store</property>
<property name="search_column">0</property>
<child internal-child="selection">
<object class="GtkTreeSelection">
<property name="mode">multiple</property>
<signal name="changed" handler="_on_selection_changed" swapped="no"/>
</object>
</child>
<child>
<object class="GtkTreeViewColumn">
<property name="resizable">True</property>
<property name="sizing">autosize</property>
<property name="title">Jabber-ID</property>
<property name="expand">True</property>
<property name="sort_indicator">True</property>
<property name="sort_column_id">0</property>
<child>
<object class="GtkCellRendererText">
<property name="placeholder_text">user@example.org</property>
<signal name="edited" handler="_on_jid_edited" swapped="no"/>
</object>
<attributes>
<attribute name="editable">6</attribute>
<attribute name="text">0</attribute>
</attributes>
</child>
</object>
</child>
<child>
<object class="GtkTreeViewColumn" id="reserved_name_column">
<property name="resizable">True</property>
<property name="sizing">autosize</property>
<property name="title" translatable="yes">Reserved Name</property>
<property name="sort_indicator">True</property>
<property name="sort_column_id">1</property>
<child>
<object class="GtkCellRendererText">
<signal name="edited" handler="_on_nick_edited" swapped="no"/>
</object>
<attributes>
<attribute name="editable">6</attribute>
<attribute name="text">1</attribute>
</attributes>
</child>
</object>
</child>
<child>
<object class="GtkTreeViewColumn" id="role_column">
<property name="visible">False</property>
<property name="resizable">True</property>
<property name="sizing">autosize</property>
<property name="min_width">100</property>
<property name="title" translatable="yes">Role</property>
<property name="sort_indicator">True</property>
<property name="sort_column_id">2</property>
<child>
<object class="GtkCellRendererText"/>
<attributes>
<attribute name="text">2</attribute>
</attributes>
</child>
</object>
</child>
<child>
<object class="GtkTreeViewColumn">
<property name="resizable">True</property>
<property name="sizing">autosize</property>
<property name="min_width">120</property>
<property name="title" translatable="yes">Affiliation</property>
<property name="sort_indicator">True</property>
<property name="sort_column_id">3</property>
<child>
<object class="GtkCellRendererCombo">
<property name="has_entry">False</property>
<property name="model">combo_store</property>
<property name="text_column">0</property>
<signal name="changed" handler="_on_affiliation_changed" swapped="no"/>
</object>
<attributes>
<attribute name="editable">5</attribute>
<attribute name="text">4</attribute>
</attributes>
</child>
</object>
</child>
</object>
</child>
<style>
<class name="margin-12"/>
</style>
</object>
<packing>
<property name="left_attach">0</property>
<property name="top_attach">0</property>
</packing>
</child>
</object>
<packing>
<property name="name">affiliation</property>
<property name="title" translatable="yes">Affiliations</property>
<property name="position">1</property>
</packing>
</child>
<child>
<object class="GtkGrid">
<property name="can_focus">False</property>
<property name="no_show_all">True</property>
<child>
<object class="GtkScrolledWindow">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="hexpand">True</property>
<property name="vexpand">True</property>
<property name="shadow_type">in</property>
<child>
<object class="GtkTreeView" id="outcast_treeview">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="model">outcast_store</property>
<property name="search_column">0</property>
<child internal-child="selection">
<object class="GtkTreeSelection">
<property name="mode">multiple</property>
<signal name="changed" handler="_on_selection_changed" swapped="no"/>
</object>
</child>
<child>
<object class="GtkTreeViewColumn">
<property name="resizable">True</property>
<property name="sizing">autosize</property>
<property name="title">Jabber-ID</property>
<property name="expand">True</property>
<property name="sort_indicator">True</property>
<property name="sort_column_id">0</property>
<child>
<object class="GtkCellRendererText">
<property name="placeholder_text">user@example.org</property>
<signal name="edited" handler="_on_outcast_jid_edited" swapped="no"/>
</object>
<attributes>
<attribute name="editable">6</attribute>
<attribute name="text">0</attribute>
</attributes>
</child>
</object>
</child>
<child>
<object class="GtkTreeViewColumn">
<property name="resizable">True</property>
<property name="sizing">autosize</property>
<property name="min_width">250</property>
<property name="title" translatable="yes">Reason</property>
<property name="sort_indicator">True</property>
<property name="sort_column_id">1</property>
<child>
<object class="GtkCellRendererText">
<property name="placeholder_text">spam</property>
<signal name="edited" handler="_on_reason_edited" swapped="no"/>
</object>
<attributes>
<attribute name="editable">6</attribute>
<attribute name="text">1</attribute>
</attributes>
</child>
</object>
</child>
</object>
</child>
<style>
<class name="margin-12"/>
</style>
</object>
<packing>
<property name="left_attach">0</property>
<property name="top_attach">0</property>
</packing>
</child>
</object>
<packing>
<property name="name">outcast</property>
<property name="title" translatable="yes">Ban List</property>
<property name="position">2</property>
</packing>
</child>
</object>
<packing>
<property name="left_attach">1</property>
<property name="top_attach">0</property>
</packing>
</child>
<child>
<object class="GtkStackSidebar">
<property name="width_request">140</property>
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="stack">stack</property>
</object>
<packing>
<property name="left_attach">0</property>
<property name="top_attach">0</property>
<property name="height">2</property>
</packing>
</child>
</object>
</interface>

View File

@ -146,6 +146,14 @@ list.settings > row > box {
#StartChatListBox > row { padding: 10px 20px 10px 10px; }
#StartChatListBox > row:not(.activatable) label { color: @insensitive_fg_color }
/* GroupchatConfig */
#GroupchatConfig buttonbox { margin: 0px 12px 12px 12px; }
#GroupchatConfig stack { border-bottom: 1px solid; border-color: @borders;}
#GroupchatConfig stacksidebar > scrolledwindow {
background-color:@theme_base_color;
border-bottom: 1px solid;
border-color: @borders;}
/* Popover Treeview */
.popover_treeview { border-radius: 3px; background-color: @theme_bg_color; }
.popover_treeview { padding: 6px; }
@ -211,6 +219,7 @@ list.settings > row > box {
/* Padding/Margins */
.margin-top6 { margin-top: 6px; }
.margin-12 { margin: 12px; }
/* Treeview */
treeview.space { padding: 6px; }

View File

@ -728,18 +728,21 @@ class GroupchatControl(ChatControlBase):
_('You may also enter an alternate venue:'), ok_handler=on_ok,
transient_for=self.parent_win.window)
def _on_configure_room(self, action, param):
c = app.contacts.get_gc_contact(
def _on_configure_room(self, _action, _param):
contact = app.contacts.get_gc_contact(
self.account, self.room_jid, self.nick)
if c.affiliation == 'owner':
if contact.affiliation == 'owner':
con = app.connections[self.account]
con.get_module('MUC').request_config(self.room_jid)
elif c.affiliation == 'admin':
if self.room_jid not in app.interface.instances[self.account][
'gc_config']:
app.interface.instances[self.account]['gc_config'][
self.room_jid] = GroupchatConfig(self.account,
self.room_jid)
elif contact.affiliation == 'admin':
win = app.get_app_window(
'GroupchatConfig', self.account, self.room_jid)
if win is not None:
win.present()
else:
GroupchatConfig(self.account,
self.room_jid,
contact.affiliation)
def _on_bookmark_room(self, action, param):
"""

View File

@ -12,237 +12,365 @@
# You should have received a copy of the GNU General Public License
# along with Gajim. If not, see <http://www.gnu.org/licenses/>.
import logging
import nbxmpp
from gi.repository import Gtk
from gajim.common import app
from gajim.common.i18n import _
from gajim.common.const import MUCUser
from gajim.common.caps_cache import muc_caps_cache
from gajim import dataforms_widget
from gajim.gtk.dialogs import ErrorDialog
from gajim.gtk.dataform import DataFormWidget
from gajim.gtk.util import get_builder
from gajim.gtk.dialogs import InputDialog
log = logging.getLogger('gajim.gtk.groupchat_config')
class GroupchatConfig:
def __init__(self, account, room_jid, form=None):
class GroupchatConfig(Gtk.ApplicationWindow):
def __init__(self, account, jid, own_affiliation, form=None):
Gtk.ApplicationWindow.__init__(self)
self.set_application(app.app)
self.set_position(Gtk.WindowPosition.CENTER)
self.set_show_menubar(False)
self.set_title(_('Group Chat Configuration'))
self.account = account
self.room_jid = room_jid
self.form = form
self.remove_button = {}
self.affiliation_treeview = {}
self.start_users_dict = {} # list at the beginning
self.affiliation_labels = {
'outcast': _('Ban List'),
'member': _('Member List'),
'owner': _('Owner List'),
'admin':_('Administrator List')
}
self.jid = jid
self._own_affiliation = own_affiliation
self._ui = get_builder('data_form_window.ui', ['data_form_window'])
self.window = self._ui.data_form_window
self.window.set_transient_for(app.interface.roster.window)
self._ui = get_builder('groupchat_config.ui')
self.add(self._ui.grid)
if self.form:
self.data_form_widget = dataforms_widget.DataFormWidget(self.form)
# hide scrollbar of this data_form_widget, we already have in this
# widget
sw = self.data_form_widget.xml.get_object(
'single_form_scrolledwindow')
sw.set_policy(Gtk.PolicyType.NEVER, Gtk.PolicyType.NEVER)
if self.form.title:
self._ui.title_label.set_text(self.form.title)
else:
self._ui.title_hseparator.set_no_show_all(True)
self._ui.title_hseparator.hide()
# Activate Add button only for Admins and Owners
if self._own_affiliation in ('admin', 'owner'):
self._ui.add_button.set_sensitive(True)
self._ui.add_button.set_tooltip_text('')
self.data_form_widget.show()
self._ui.config_vbox.pack_start(self.data_form_widget, True, True, 0)
visible = muc_caps_cache.supports(jid, nbxmpp.NS_REGISTER)
self._ui.reserved_name_column.set_visible(visible)
self._form = form
self._affiliations = {}
self._new_affiliations = {}
con = app.connections[self.account]
for affiliation in ('owner', 'admin', 'member', 'outcast'):
con.get_module('MUC').get_affiliation(
self.jid,
affiliation,
self._on_affiliations_received,
self._on_affiliations_error)
if form is not None:
self._ui.stack.set_visible_child_name('config')
self._data_form_widget = DataFormWidget(form)
self._ui.config_grid.add(self._data_form_widget)
# self._ui.stack.set_visible_child_name('affiliation')
else:
self._ui.title_label.set_no_show_all(True)
self._ui.title_label.hide()
self._ui.title_hseparator.set_no_show_all(True)
self._ui.title_hseparator.hide()
self._ui.config_hseparator.set_no_show_all(True)
self._ui.config_hseparator.hide()
# Draw the edit affiliation list things
add_on_vbox = self._ui.add_on_vbox
for affiliation in self.affiliation_labels:
self.start_users_dict[affiliation] = {}
hbox = Gtk.HBox(spacing=5)
add_on_vbox.pack_start(hbox, False, True, 0)
label = Gtk.Label(label=self.affiliation_labels[affiliation])
hbox.pack_start(label, False, True, 0)
bb = Gtk.HButtonBox()
bb.set_layout(Gtk.ButtonBoxStyle.END)
bb.set_spacing(5)
hbox.pack_start(bb, True, True, 0)
add_button = Gtk.Button(stock=Gtk.STOCK_ADD)
add_button.connect(
'clicked', self.on_add_button_clicked, affiliation)
bb.pack_start(add_button, True, True, 0)
self.remove_button[affiliation] = Gtk.Button(stock=Gtk.STOCK_REMOVE)
self.remove_button[affiliation].set_sensitive(False)
self.remove_button[affiliation].connect(
'clicked', self.on_remove_button_clicked, affiliation)
bb.pack_start(self.remove_button[affiliation], True, True, 0)
# jid, reason, nick, role
liststore = Gtk.ListStore(str, str, str, str)
self.affiliation_treeview[affiliation] = Gtk.TreeView(liststore)
self.affiliation_treeview[affiliation].get_selection().set_mode(
Gtk.SelectionMode.MULTIPLE)
self.affiliation_treeview[affiliation].connect(
'cursor-changed',
self.on_affiliation_treeview_cursor_changed,
affiliation)
renderer = Gtk.CellRendererText()
col = Gtk.TreeViewColumn(_('JID'), renderer)
col.add_attribute(renderer, 'text', 0)
col.set_resizable(True)
col.set_sort_column_id(0)
self.affiliation_treeview[affiliation].append_column(col)
if affiliation == 'outcast':
renderer = Gtk.CellRendererText()
renderer.set_property('editable', True)
renderer.connect('edited', self.on_cell_edited)
col = Gtk.TreeViewColumn(_('Reason'), renderer)
col.add_attribute(renderer, 'text', 1)
col.set_resizable(True)
col.set_sort_column_id(1)
self.affiliation_treeview[affiliation].append_column(col)
elif affiliation == 'member':
renderer = Gtk.CellRendererText()
col = Gtk.TreeViewColumn(_('Nick'), renderer)
col.add_attribute(renderer, 'text', 2)
col.set_resizable(True)
col.set_sort_column_id(2)
self.affiliation_treeview[affiliation].append_column(col)
renderer = Gtk.CellRendererText()
col = Gtk.TreeViewColumn(_('Role'), renderer)
col.add_attribute(renderer, 'text', 3)
col.set_resizable(True)
col.set_sort_column_id(3)
self.affiliation_treeview[affiliation].append_column(col)
sw = Gtk.ScrolledWindow()
sw.add(self.affiliation_treeview[affiliation])
add_on_vbox.pack_start(sw, True, True, 0)
con = app.connections[self.account]
con.get_module('MUC').get_affiliation(self.room_jid, affiliation)
self._ui.stack.get_child_by_name('config').hide()
self._ui.stack.get_child_by_name('config').set_no_show_all(True)
self._ui.stack.set_visible_child_name('affiliation')
self._ui.connect_signals(self)
self.window.connect('delete-event', self.on_cancel_button_clicked)
self.window.show_all()
self.show_all()
self._ui.stack.notify('visible-child-name')
def on_cancel_button_clicked(self, *args):
if self.form:
con = app.connections[self.account]
con.get_module('MUC').cancel_config(self.room_jid)
self.window.destroy()
def _get_current_treeview(self):
page_name = self._ui.stack.get_visible_child_name()
return getattr(self._ui, '%s_treeview' % page_name)
def on_cell_edited(self, _cell, path, new_text):
model = self.affiliation_treeview['outcast'].get_model()
new_text = new_text
iter_ = model.get_iter(path)
model[iter_][1] = new_text
def on_add_button_clicked(self, _widget, affiliation):
if affiliation == 'outcast':
title = _('Banning…')
#You can move '\n' before user@domain if that line is TOO BIG
prompt = _('<b>Whom do you want to ban?</b>\n\n')
elif affiliation == 'member':
title = _('Adding Member…')
prompt = _('<b>Whom do you want to make a member?</b>\n\n')
elif affiliation == 'owner':
title = _('Adding Owner…')
prompt = _('<b>Whom do you want to make an owner?</b>\n\n')
def _on_add(self, *args):
page_name = self._ui.stack.get_visible_child_name()
if page_name == 'outcast':
affiliation_edit, jid_edit = self._allowed_to_edit('outcast')
text = None
affiliation = 'outcast'
else:
title = _('Adding Administrator…')
prompt = _('<b>Whom do you want to make an administrator?</b>\n\n')
prompt += _(
'Can be one of the following:\n'
'1. user@domain/resource (only that resource matches).\n'
'2. user@domain (any resource matches).\n'
'3. domain/resource (only that resource matches).\n'
'4. domain (the domain itself matches, as does any user@domain,\n'
'domain/resource, or address containing a subdomain).')
affiliation_edit, jid_edit = self._allowed_to_edit('member')
text = _('Member')
affiliation = 'member'
def on_ok(jid):
if not jid:
return
model = self.affiliation_treeview[affiliation].get_model()
model.append((jid, '', '', ''))
InputDialog(title, prompt, ok_handler=on_ok)
treeview = self._get_current_treeview()
treeview.get_model().append([None,
None,
None,
affiliation,
text,
affiliation_edit,
jid_edit])
def on_remove_button_clicked(self, _widget, affiliation):
selection = self.affiliation_treeview[affiliation].get_selection()
model, paths = selection.get_selected_rows()
row_refs = []
# Scroll to added row
row = treeview.get_model()[-1]
treeview.scroll_to_cell(row.path, None, False, 0, 0)
treeview.get_selection().unselect_all()
treeview.get_selection().select_path(row.path)
def _on_remove(self, *args):
treeview = self._get_current_treeview()
model, paths = treeview.get_selection().get_selected_rows()
owner_count = self._get_owner_count()
references = []
for path in paths:
row_refs.append(Gtk.TreeRowReference.new(model, path))
for row_ref in row_refs:
path = row_ref.get_path()
iter_ = model.get_iter(path)
if model[path][MUCUser.AFFILIATION] == 'owner':
if owner_count < 2:
# There must be at least one owner
ErrorDialog(_('Error'),
_('A Group Chat needs at least one Owner'))
return
owner_count -= 1
references.append(Gtk.TreeRowReference.new(model, path))
for ref in references:
iter_ = model.get_iter(ref.get_path())
model.remove(iter_)
self.remove_button[affiliation].set_sensitive(False)
def on_affiliation_treeview_cursor_changed(self, _widget, affiliation):
self.remove_button[affiliation].set_sensitive(True)
def _on_jid_edited(self, _renderer, path, new_text):
old_text = self._ui.affiliation_store[path][MUCUser.JID]
if new_text == old_text:
return
def affiliation_list_received(self, users_dict):
"""
Fill the affiliation treeview
"""
for jid in users_dict:
affiliation = users_dict[jid]['affiliation']
if affiliation not in self.affiliation_labels.keys():
# Unknown affiliation or 'none' affiliation, do not show it
continue
self.start_users_dict[affiliation][jid] = users_dict[jid]
tv = self.affiliation_treeview[affiliation]
model = tv.get_model()
reason = users_dict[jid].get('reason', '')
nick = users_dict[jid].get('nick', '')
role = users_dict[jid].get('role', '')
model.append((jid, reason, nick, role))
if self._jid_exists(new_text):
self._raise_error()
return
def on_data_form_window_destroy(self, _widget):
del app.interface.instances[self.account]['gc_config'][self.room_jid]
self._ui.affiliation_store[path][MUCUser.JID] = new_text
def on_ok_button_clicked(self, _widget):
if self.form:
form = self.data_form_widget.data_form
def _on_outcast_jid_edited(self, _renderer, path, new_text):
old_text = self._ui.outcast_store[path][MUCUser.JID]
if new_text == old_text:
return
if self._jid_exists(new_text):
self._raise_error()
return
self._ui.outcast_store[path][MUCUser.JID] = new_text
self._ui.outcast_store[path][MUCUser.AFFILIATION] = 'outcast'
def _on_nick_edited(self, _renderer, path, new_text):
self._ui.affiliation_store[path][MUCUser.NICK] = new_text
def _on_reason_edited(self, _renderer, path, new_text):
self._ui.outcast_store[path][MUCUser.REASON] = new_text
def _on_affiliation_changed(self, cell_renderer_combo,
path_string, new_iter):
combo_store = cell_renderer_combo.get_property('model')
affiliation_text = combo_store.get_value(new_iter, 0)
affiliation = combo_store.get_value(new_iter, 1)
store = self._ui.affiliation_treeview.get_model()
store[path_string][MUCUser.AFFILIATION] = affiliation
store[path_string][MUCUser.AFFILIATION_TEXT] = affiliation_text
def _on_selection_changed(self, tree_selection):
sensitive = bool(tree_selection.count_selected_rows())
selected_affiliations = self._get_selected_affiliations(tree_selection)
self._set_remove_button_state(sensitive, selected_affiliations)
def _jid_exists(self, jid):
stores = [self._ui.affiliation_store, self._ui.outcast_store]
for store in stores:
for row in store:
if row[MUCUser.JID] == jid:
return True
return False
@staticmethod
def _get_selected_affiliations(tree_selection):
model, paths = tree_selection.get_selected_rows()
selected_affiliations = set()
for path in paths:
selected_affiliations.add(model[path][MUCUser.AFFILIATION])
return selected_affiliations
def _on_switch_page(self, stack, _pspec):
page_name = stack.get_visible_child_name()
self._set_button_box_state(page_name)
if page_name == 'config':
return
treeview = getattr(self._ui, '%s_treeview' % page_name)
sensitive = bool(treeview.get_selection().count_selected_rows())
selected_affiliations = self._get_selected_affiliations(
treeview.get_selection())
self._set_remove_button_state(sensitive, selected_affiliations)
def _set_button_box_state(self, page_name):
affiliation = self._own_affiliation in ('admin', 'owner')
page = page_name != 'config'
self._ui.treeview_buttonbox.set_visible(affiliation and page)
def _set_remove_button_state(self, sensitive, selected_affiliations):
if self._own_affiliation not in ('admin', 'owner'):
self._ui.remove_button.set_sensitive(False)
return
self._ui.remove_button.set_tooltip_text('')
if not sensitive:
self._ui.remove_button.set_sensitive(False)
return
if self._own_affiliation == 'owner':
self._ui.remove_button.set_sensitive(True)
return
if set(['owner', 'admin']).intersection(selected_affiliations):
self._ui.remove_button.set_sensitive(False)
self._ui.remove_button.set_tooltip_text(
_('You are not allowed to modify the affiliation '
'of Admins and Owners'))
return
self._ui.remove_button.set_sensitive(True)
def _get_owner_count(self):
owner_count = 0
for row in self._ui.affiliation_store:
if row[MUCUser.AFFILIATION] == 'owner':
owner_count += 1
return owner_count
def _allowed_to_edit(self, affiliation):
if self._own_affiliation == 'owner':
return True, True
if self._own_affiliation == 'admin':
if affiliation in ('admin', 'owner'):
return False, False
return False, True
return False, False
def _on_ok(self, *args):
if self._own_affiliation in ('admin', 'owner'):
self._set_affiliations()
if self._form is not None and self._own_affiliation == 'owner':
form = self._data_form_widget.get_submit_form()
con = app.connections[self.account]
con.get_module('MUC').set_config(self.room_jid, form)
for affiliation in self.affiliation_labels:
users_dict = {}
actual_jid_list = []
model = self.affiliation_treeview[affiliation].get_model()
iter_ = model.get_iter_first()
# add new jid
while iter_:
jid = model[iter_][0]
actual_jid_list.append(jid)
if jid not in self.start_users_dict[affiliation] or \
(affiliation == 'outcast' and 'reason' in self.start_users_dict[
affiliation][jid] and self.start_users_dict[affiliation][jid]\
['reason'] != model[iter_][1]):
users_dict[jid] = {'affiliation': affiliation}
if affiliation == 'outcast':
users_dict[jid]['reason'] = model[iter_][1]
iter_ = model.iter_next(iter_)
# remove removed one
for jid in self.start_users_dict[affiliation]:
if jid not in actual_jid_list:
users_dict[jid] = {'affiliation': 'none'}
if users_dict:
con = app.connections[self.account]
con.get_module('MUC').set_affiliation(
self.room_jid, users_dict)
self.window.destroy()
con.get_module('MUC').set_config(self.jid, form)
self.destroy()
def _get_diff(self):
stores = [self._ui.affiliation_store, self._ui.outcast_store]
self._new_affiliations = {}
for store in stores:
for row in store:
if not row[MUCUser.JID]:
# Ignore empty JID field
continue
attr = 'nick'
if row[MUCUser.AFFILIATION] == 'outcast':
attr = 'reason'
self._new_affiliations[row[MUCUser.JID]] = {
'affiliation': row[MUCUser.AFFILIATION],
attr: row[MUCUser.NICK_OR_REASON]}
old_jids = set(self._affiliations.keys())
new_jids = set(self._new_affiliations.keys())
remove = old_jids - new_jids
add = new_jids - old_jids
modified = new_jids - remove - add
return add, remove, modified
def _on_cancel(self, *args):
if self._form and self._own_affiliation == 'owner':
con = app.connections[self.account]
con.get_module('MUC').cancel_config(self.jid)
self.destroy()
def _set_affiliations(self):
add, remove, modified = self._get_diff()
diff_dict = {}
for jid in add:
diff_dict[jid] = self._new_affiliations[jid]
for jid in remove:
diff_dict[jid] = {'affiliation': 'none'}
for jid in modified:
if self._new_affiliations[jid] == self._affiliations[jid]:
# Not modified
continue
diff_dict[jid] = self._new_affiliations[jid]
if self._new_affiliations[jid]['affiliation'] == 'outcast':
# New affiliation is outcast, check if the reason changed.
# In case the affiliation was 'admin', 'owner' or 'member'
# before, there is no reason.
new_reason = self._new_affiliations[jid]['reason']
old_reason = self._affiliations[jid].get('reason')
if new_reason == old_reason:
diff_dict[jid].pop('reason', None)
else:
# New affiliation is not outcast, check if the nick has changed.
# In case the affiliation was 'outcast' there is no nick.
new_nick = self._new_affiliations[jid]['nick']
old_nick = self._affiliations[jid].get('nick')
if new_nick == old_nick:
diff_dict[jid].pop('nick', None)
if not diff_dict:
# No changes were made
return
con = app.connections[self.account]
con.get_module('MUC').set_affiliation(self.jid, diff_dict)
def _on_affiliations_error(self, affiliation, error):
log.info('Error while requesting %s affiliations: %s',
affiliation, error)
def _on_affiliations_received(self, _account, _room_jid,
affiliation, users):
if affiliation == 'outcast':
self._ui.stack.get_child_by_name('outcast').show()
for jid, attrs in users.items():
affiliation_edit, jid_edit = self._allowed_to_edit(affiliation)
if affiliation == 'outcast':
reason = attrs.get('reason')
self._ui.outcast_store.append(
[jid,
reason,
None,
affiliation,
None,
affiliation_edit,
jid_edit])
self._affiliations[jid] = {'affiliation': affiliation,
'reason': reason}
else:
nick = attrs.get('nick')
role = attrs.get('role')
self._ui.affiliation_store.append(
[jid,
nick,
role,
affiliation,
_(affiliation.capitalize()),
affiliation_edit,
jid_edit])
self._affiliations[jid] = {'affiliation': affiliation,
'nick': nick}
if role is not None:
self._ui.role_column.set_visible(True)
@staticmethod
def _raise_error():
ErrorDialog(_('Error'),
_('An entry with this Jabber-ID already exists'))

View File

@ -601,16 +601,12 @@ class Interface:
_('%(jid)s has been invited in this room') % {
'jid': jid}, graphics=False)
del app.automatic_rooms[account][obj.jid]
elif obj.jid not in self.instances[account]['gc_config']:
self.instances[account]['gc_config'][obj.jid] = \
GroupchatConfig(account, obj.jid, obj.dataform)
def handle_event_gc_affiliation(self, obj):
#('GC_AFFILIATION', account, (room_jid, users_dict))
account = obj.conn.name
if obj.room_jid in self.instances[account]['gc_config']:
self.instances[account]['gc_config'][obj.room_jid].\
affiliation_list_received(obj.users_dict)
else:
win = app.get_app_window('GroupchatConfig', account, obj.jid)
if win is not None:
win.present()
else:
GroupchatConfig(account, obj.jid, 'owner', obj.dataform)
def handle_event_gc_decline(self, obj):
gc_control = self.msg_win_mgr.get_gc_control(obj.room_jid, obj.account)
@ -1531,7 +1527,6 @@ class Interface:
'message-not-sent': [self.handle_event_msgnotsent],
'message-sent': [self.handle_event_msgsent],
'metacontacts-received': [self.handle_event_metacontacts],
'muc-admin-received': [self.handle_event_gc_affiliation],
'muc-owner-received': [self.handle_event_gc_config],
'oauth2-credentials-required': [self.handle_oauth2_credentials],
'our-show': [self.handle_event_status],