Commit 970d6f8c authored by Philipp Hörist's avatar Philipp Hörist

New style for ChatControl

- Move ActionBar into HeaderMenu
- Make Design of ChatControl look cleaner
- Hide the Roster in Groupchats per default
- Add Button to hide/show Roster in Groupchats
- Move Groupchat topic into popover
- Display Avatars on the right side of the ChatControl and status on the
left
- Add a default Avatar for contacts that have none
parent 398ad0ee
This diff is collapsed.
......@@ -44,7 +44,6 @@ from gajim import notify
import re
from gajim import emoticons
from gajim.scrolled_window import ScrolledWindow
from gajim.common import events
from gajim.common import app
from gajim.common import helpers
......@@ -256,28 +255,21 @@ class ChatControlBase(MessageControl, ChatCommandProcessor, CommandTools):
MessageControl.__init__(self, type_id, parent_win, widget_name,
contact, acct, resource=resource)
widget = self.xml.get_object('history_button')
# set document-open-recent icon for history button
if gtkgui_helpers.gtk_icon_theme.has_icon('document-open-recent'):
img = self.xml.get_object('history_image')
img.set_from_icon_name('document-open-recent', Gtk.IconSize.MENU)
id_ = widget.connect('clicked', self._on_history_menuitem_activate)
self.handlers[id_] = widget
# Create banner and connect signals
widget = self.xml.get_object('banner_eventbox')
id_ = widget.connect('button-press-event',
self._on_banner_eventbox_button_press_event)
self.handlers[id_] = widget
if self.TYPE_ID != message_control.TYPE_GC:
# Create banner and connect signals
widget = self.xml.get_object('banner_eventbox')
id_ = widget.connect('button-press-event',
self._on_banner_eventbox_button_press_event)
self.handlers[id_] = widget
self.urlfinder = re.compile(
r"(www\.(?!\.)|[a-z][a-z0-9+.-]*://)[^\s<>'\"]+[^!,\.\s<>\)'\"\]]")
self.banner_status_label = self.xml.get_object('banner_label')
id_ = self.banner_status_label.connect('populate_popup',
self.on_banner_label_populate_popup)
self.handlers[id_] = self.banner_status_label
if self.banner_status_label is not None:
id_ = self.banner_status_label.connect('populate_popup',
self.on_banner_label_populate_popup)
self.handlers[id_] = self.banner_status_label
# Init DND
self.TARGET_TYPE_URI_LIST = 80
......@@ -332,9 +324,8 @@ class ChatControlBase(MessageControl, ChatCommandProcessor, CommandTools):
self.msg_scrolledwindow = ScrolledWindow()
self.msg_scrolledwindow.set_max_content_height(100)
self.msg_scrolledwindow.set_min_content_height(23)
self.msg_scrolledwindow.set_propagate_natural_height(True)
self.msg_scrolledwindow.get_style_context().add_class('scrolledtextview')
self.msg_scrolledwindow.set_property('shadow_type', Gtk.ShadowType.IN)
self.msg_scrolledwindow.add(self.msg_textview)
......@@ -417,6 +408,28 @@ class ChatControlBase(MessageControl, ChatCommandProcessor, CommandTools):
action.connect("change-state", self.change_encryption)
self.parent_win.window.add_action(action)
action = Gio.SimpleAction.new(
'browse-history-%s' % self.control_id, GLib.VariantType.new('s'))
action.connect('activate', self._on_history)
self.parent_win.window.add_action(action)
# Actions
def _on_history(self, action, param):
"""
When history menuitem is pressed: call history window
"""
jid = param.get_string()
if jid == 'none':
jid = self.contact.jid
if 'logs' in app.interface.instances:
app.interface.instances['logs'].window.present()
app.interface.instances['logs'].open_history(jid, self.account)
else:
app.interface.instances['logs'] = \
history_window.HistoryWindow(jid, self.account)
def change_encryption(self, action, param):
encryption = param.get_string()
if encryption == 'disabled':
......@@ -549,6 +562,7 @@ class ChatControlBase(MessageControl, ChatCommandProcessor, CommandTools):
menu.show_all()
def on_quote(self, widget, text):
self.msg_textview.remove_placeholder()
text = '>' + text.replace('\n', '\n>') + '\n'
message_buffer = self.msg_textview.get_buffer()
message_buffer.insert_at_cursor(text)
......@@ -1283,13 +1297,6 @@ class ChatControlBase(MessageControl, ChatCommandProcessor, CommandTools):
else:
widget.show_all()
def chat_buttons_set_visible(self, state):
"""
Toggle chat buttons
"""
MessageControl.chat_buttons_set_visible(self, state)
self.widget_set_visible(self.xml.get_object('actions_hbox'), state)
def got_connected(self):
self.msg_textview.set_sensitive(True)
self.msg_textview.set_editable(True)
......@@ -1302,3 +1309,19 @@ class ChatControlBase(MessageControl, ChatCommandProcessor, CommandTools):
self.no_autonegotiation = False
self.update_toolbar()
class ScrolledWindow(Gtk.ScrolledWindow):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
def do_get_preferred_height(self):
min_height, natural_height = Gtk.ScrolledWindow.do_get_preferred_height(self)
child = self.get_child()
if natural_height and self.get_max_content_height() > -1 and child:
_, child_nat_height = child.get_preferred_height()
if natural_height > child_nat_height:
if child_nat_height < 26:
return 26, 26
return min_height, natural_height
......@@ -41,12 +41,6 @@ class StandardCommonCommands(CommandContainer):
AUTOMATIC = True
HOSTS = ChatCommands, PrivateChatCommands, GroupChatCommands
@command
@doc(_("Hide the chat buttons"))
def compact(self):
new_status = not self.hide_chat_buttons
self.chat_buttons_set_visible(new_status)
@command(overlap=True)
@doc(_("Show help on a given command or a list of available commands if -a is given"))
def help(self, command=None, all=False):
......
......@@ -264,7 +264,6 @@ class Config:
'show_roster_on_startup':[opt_str, 'always', _('Show roster on startup.\n\'always\' - Always show roster.\n\'never\' - Never show roster.\n\'last_state\' - Restore the last state roster.')],
'show_avatar_in_chat': [opt_bool, True, _('If False, you will no longer see the avatar in the chat window.')],
'escape_key_closes': [opt_bool, True, _('If True, pressing the escape key closes a tab/window.')],
'compact_view': [opt_bool, False, _('Hides the buttons in chat windows.')],
'hide_groupchat_banner': [opt_bool, False, _('Hides the banner in a group chat window')],
'hide_chat_banner': [opt_bool, False, _('Hides the banner in two persons chat window')],
'hide_groupchat_occupants_list': [opt_bool, False, _('Hides the group chat occupants list in group chat window.')],
......
......@@ -2342,6 +2342,13 @@ class Connection(CommonConnection, ConnectionHandlers):
if rule['type'] == 'group':
roster.draw_group(rule['value'], self.name)
def bookmarks_available(self):
if self.private_storage_supported:
return True
if self.pubsub_publish_options_supported:
return True
return False
def _request_bookmarks_xml(self):
if not app.account_is_connected(self.name):
return
......
......@@ -30,8 +30,8 @@ class OptionType(IntEnum):
class AvatarSize(IntEnum):
ROSTER = 32
CHAT = 48
NOTIFICATION = 48
CHAT = 52
PROFILE = 64
TOOLTIP = 125
VCARD = 200
......
......@@ -187,10 +187,6 @@ class PreferencesWindow:
else:
show_roster_combobox.set_active(0)
# Compact View
st = app.config.get('compact_view')
self.xml.get_object('compact_view_checkbutton').set_active(st)
# Ignore XHTML
st = app.config.get('ignore_incoming_xhtml')
self.xml.get_object('xhtml_checkbutton').set_active(st)
......@@ -657,12 +653,6 @@ class PreferencesWindow:
config_type = c_config.opt_show_roster_on_startup[active]
app.config.set('show_roster_on_startup', config_type)
def on_compact_view_checkbutton_toggled(self, widget):
active = widget.get_active()
for ctrl in self._get_all_controls():
ctrl.chat_buttons_set_visible(active)
app.config.set('compact_view', active)
def on_xhtml_checkbutton_toggled(self, widget):
self.on_checkbutton_toggled(widget, 'ignore_incoming_xhtml')
helpers.update_optional_features()
......
This diff is collapsed.
This diff is collapsed.
<?xml version="1.0" encoding="UTF-8"?>
<!-- Generated with glade 3.18.3 -->
<!-- Generated with glade 3.20.0 -->
<interface>
<requires lib="gtk+" version="3.12"/>
<object class="GtkEventBox" id="chat_tab_ebox">
......@@ -28,10 +28,10 @@
<property name="width_request">70</property>
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="xalign">0</property>
<property name="use_markup">True</property>
<property name="ellipsize">end</property>
<property name="max_width_chars">9</property>
<property name="xalign">0</property>
</object>
<packing>
<property name="expand">False</property>
......@@ -64,15 +64,35 @@
</object>
</child>
</object>
<object class="GtkHeaderBar" id="headerbar">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="show_close_button">True</property>
<child>
<object class="GtkMenuButton" id="header_menu">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">True</property>
<child>
<object class="GtkImage">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="icon_name">open-menu-symbolic</property>
</object>
</child>
</object>
</child>
</object>
<object class="GtkApplicationWindow" id="message_window">
<property name="name">MessageWindow</property>
<property name="can_focus">False</property>
<property name="default_width">480</property>
<property name="default_height">440</property>
<property name="show_menubar">False</property>
<child>
<object class="GtkNotebook" id="notebook">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="margin_top">2</property>
<property name="scrollable">True</property>
</object>
</child>
......
......@@ -426,23 +426,6 @@
<property name="top_attach">2</property>
</packing>
</child>
<child>
<object class="GtkCheckButton" id="compact_view_checkbutton">
<property name="label" translatable="yes">Ma_ke message windows compact</property>
<property name="can_focus">True</property>
<property name="receives_default">False</property>
<property name="tooltip_text" translatable="yes">Hide all buttons in chat windows</property>
<property name="use_underline">True</property>
<property name="xalign">0</property>
<property name="draw_indicator">True</property>
<signal name="toggled" handler="on_compact_view_checkbutton_toggled" swapped="no"/>
</object>
<packing>
<property name="left_attach">0</property>
<property name="top_attach">3</property>
<property name="width">2</property>
</packing>
</child>
<child>
<object class="GtkCheckButton" id="xhtml_checkbutton">
<property name="label" translatable="yes">_Ignore rich content in incoming messages</property>
......@@ -457,7 +440,7 @@
</object>
<packing>
<property name="left_attach">0</property>
<property name="top_attach">4</property>
<property name="top_attach">3</property>
<property name="width">2</property>
</packing>
</child>
......@@ -474,7 +457,7 @@
</object>
<packing>
<property name="left_attach">0</property>
<property name="top_attach">5</property>
<property name="top_attach">4</property>
<property name="width">2</property>
</packing>
</child>
......@@ -490,7 +473,7 @@
</object>
<packing>
<property name="left_attach">0</property>
<property name="top_attach">6</property>
<property name="top_attach">5</property>
<property name="width">2</property>
</packing>
</child>
......@@ -555,7 +538,7 @@
</object>
<packing>
<property name="left_attach">0</property>
<property name="top_attach">7</property>
<property name="top_attach">6</property>
<property name="width">2</property>
</packing>
</child>
......
......@@ -4,6 +4,7 @@
<requires lib="gtk+" version="3.12"/>
<object class="GtkAccelGroup" id="accelgroup1"/>
<object class="GtkApplicationWindow" id="roster_window">
<property name="name">RosterWindow</property>
<property name="width_request">85</property>
<property name="height_request">200</property>
<property name="can_focus">False</property>
......@@ -115,4 +116,28 @@
</object>
</child>
</object>
<object class="GtkHeaderBar" id="headerbar">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="title">Gajim</property>
<property name="show_close_button">True</property>
<child>
<object class="GtkMenuButton" id="header_menu">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">True</property>
<property name="no_show_all">True</property>
<child>
<object class="GtkImage">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="icon_name">open-menu-symbolic</property>
</object>
</child>
</object>
<packing>
<property name="pack_type">end</property>
</packing>
</child>
</object>
</interface>
/* Gajim Application CSS File */
.msgtextview-button {
.chatcontrol-actionbar-button {
padding: 0px 5px 0px 5px;
background-color: @theme_base_color;
border: 1px solid;
border-radius: 0px;
border-color: @borders;
border: none;
}
.msgtextview-button:hover, .msgtextview-button:checked {
color: @theme_base_color;
border-color: @borders;
text-shadow: none;
-gtk-icon-shadow: none;
box-shadow: none;
.scrolled-no-border {border: none}
.scrolled-no-border undershoot.top, undershoot.bottom { background-image: none; }
.actionbar-no-border box {border: none}
.actionbar-no-border button {
padding: 0px;
background-color: @theme_base_color;
border: none;
background-image: none;
}
#MessageWindow, #RosterWindow paned { background-color: @theme_base_color; }
.scrolledtextview { border:none; }
.msgtextview-button.left { border-right: none; }
.msgtextview-button.right { border-left: none; }
.chatcontrol-separator {margin-bottom: 6px;}
.scrolledtextview { border-left:none; }
.scrolledtextview.authentication { border-right:none; }
#SubjectPopover box { padding: 10px; }
/* VCardWindow */
.VCard-GtkLinkButton { padding-left: 5px; border-left: none; }
......
......@@ -289,6 +289,7 @@ class EmoticonPopover(Gtk.Popover):
self.append_emoticon(child.get_child().get_text())
def append_emoticon(self, pix):
self.text_widget.remove_placeholder()
buffer_ = self.text_widget.get_buffer()
if buffer_.get_char_count():
buffer_.insert_at_cursor(' ')
......
......@@ -212,8 +212,15 @@ class GajimApplication(Gtk.Application):
builder = Gtk.Builder()
builder.set_translation_domain(i18n.APP)
builder.add_from_file(path)
self.set_menubar(builder.get_object("menubar"))
self.set_app_menu(builder.get_object("appmenu"))
menubar = builder.get_object("menubar")
appmenu = builder.get_object("appmenu")
if os.name != 'nt':
self.set_app_menu(appmenu)
else:
# Dont set Application Menu for Windows
# Add it to the menubar instead
menubar.prepend_submenu('Gajim', appmenu)
self.set_menubar(menubar)
def do_activate(self):
Gtk.Application.do_activate(self)
......
This diff is collapsed.
......@@ -57,6 +57,7 @@ class Color:
BLACK = Gdk.RGBA(red=0, green=0, blue=0, alpha=1)
GREEN = Gdk.RGBA(red=115/255, green=210/255, blue=22/255, alpha=1)
RED = Gdk.RGBA(red=204/255, green=0, blue=0, alpha=1)
GREY = Gdk.RGBA(red=195/255, green=195/255, blue=192/255, alpha=1)
def get_icon_pixmap(icon_name, size=16, color=None, quiet=False):
try:
......
......@@ -23,6 +23,7 @@ import gi
gi.require_version('GtkSpell', '3.0')
from gi.repository import GtkSpell
def ensure_attached(func):
def f(self, *args, **kwargs):
if self.spell:
......
......@@ -606,6 +606,65 @@ Build dynamic Application Menus
'''
def get_singlechat_menu(control_id):
singlechat_menu = [
('win.send-file-', _('Send File...')),
('win.invite-contacts-', _('Invite Contacts')),
('win.add-to-roster-', _('Add to Roster')),
('win.toggle-audio-', _('Audio Session')),
('win.toggle-video-', _('Video Session')),
('win.information-', _('Information')),
('win.browse-history-', _('History')),
]
def build_menu(preset):
menu = Gio.Menu()
for item in preset:
action_name, label = item
if action_name == 'win.browse-history-':
menu.append(label, action_name + control_id + '::none')
else:
menu.append(label, action_name + control_id)
return menu
return build_menu(singlechat_menu)
def get_groupchat_menu(control_id):
groupchat_menu = [
(_('Manage Room'), [
('win.change-subject-', _('Change Subject')),
('win.configure-', _('Configure Room')),
('win.destroy-', _('Destroy Room')),
]),
('win.change-nick-', _('Change Nick')),
('win.bookmark-', _('Bookmark Room')),
('win.request-voice-', _('Request Voice')),
('win.notify-on-message-', _('Notify on all messages')),
('win.minimize-', _('Minimize on close')),
('win.browse-history-', _('History')),
('win.disconnect-', _('Disconnect')),
]
def build_menu(preset):
menu = Gio.Menu()
for item in preset:
if isinstance(item[1], str):
action_name, label = item
if action_name == 'win.browse-history-':
menu.append(label, action_name + control_id + '::none')
else:
menu.append(label, action_name + control_id)
else:
label, sub_menu = item
# This is a submenu
submenu = build_menu(sub_menu)
menu.append_submenu(label, submenu)
return menu
return build_menu(groupchat_menu)
def get_bookmarks_menu(account, rebuild=False):
if not app.connections[account].bookmarks:
return None
......@@ -708,7 +767,11 @@ def get_account_menu(account):
def build_accounts_menu():
menubar = app.app.get_menubar()
# Accounts Submenu
acc_menu = menubar.get_item_link(0, 'submenu')
menu_position = 0
if os.name == 'nt':
menu_position = 1
acc_menu = menubar.get_item_link(menu_position, 'submenu')
acc_menu.remove_all()
accounts_list = sorted(app.contacts.get_accounts())
if not accounts_list:
......@@ -721,8 +784,8 @@ def build_accounts_menu():
acc, get_account_menu(acc))
else:
acc_menu = get_account_menu(accounts_list[0])
menubar.remove(0)
menubar.insert_submenu(0, 'Accounts', acc_menu)
menubar.remove(menu_position)
menubar.insert_submenu(menu_position, 'Accounts', acc_menu)
def build_bookmark_menu(account):
......@@ -731,8 +794,12 @@ def build_bookmark_menu(account):
if not bookmark_menu:
return
menu_position = 0
if os.name == 'nt':
menu_position = 1
# Accounts Submenu
acc_menu = menubar.get_item_link(0, 'submenu')
acc_menu = menubar.get_item_link(menu_position, 'submenu')
# We have more than one Account active
if acc_menu.get_item_link(0, 'submenu'):
......
......@@ -57,7 +57,6 @@ class MessageControl(object):
self.widget_name = widget_name
self.contact = contact
self.account = account
self.hide_chat_buttons = False
self.resource = resource
# control_id is a unique id for the control,
# its used as action name for actions that belong to a control
......@@ -175,12 +174,6 @@ class MessageControl(object):
"""
return None
def chat_buttons_set_visible(self, state):
"""
Derived classes MAY implement this
"""
self.hide_chat_buttons = state
def got_connected(self):
pass
......
......@@ -24,7 +24,6 @@
import gc
from gi.repository import Gtk
from gi.repository import GObject
from gi.repository import GLib
from gi.repository import Pango
......@@ -37,6 +36,7 @@ class MessageTextView(Gtk.TextView):
chat/groupchat windows
"""
UNDO_LIMIT = 20
PLACEHOLDER = _('Write a message..')
def __init__(self):
Gtk.TextView.__init__(self)
......@@ -64,6 +64,9 @@ class MessageTextView(Gtk.TextView):
self.color_tags = []
self.fonts_tags = []
self.other_tags = {}
self.placeholder_tag = _buffer.create_tag('placeholder')
self.placeholder_tag.set_property('foreground_rgba',
gtkgui_helpers.Color.GREY)
self.other_tags['bold'] = _buffer.create_tag('bold')
self.other_tags['bold'].set_property('weight', Pango.Weight.BOLD)
self.begin_tags['bold'] = '<strong>'
......@@ -82,6 +85,30 @@ class MessageTextView(Gtk.TextView):
self.end_tags['strike'] = '</span>'
self.connect_after('paste-clipboard', self.after_paste_clipboard)
self.connect('focus-in-event', self._on_focus_in)
self.connect('focus-out-event', self._on_focus_out)
start, end = _buffer.get_bounds()
_buffer.insert_with_tags(
start, self.PLACEHOLDER, self.placeholder_tag)
def _on_focus_in(self, *args):
buf = self.get_buffer()
start, end = buf.get_bounds()
text = buf.get_text(start, end, True)
if text == self.PLACEHOLDER:
buf.set_text('')
def _on_focus_out(self, *args):
buf = self.get_buffer()
start, end = buf.get_bounds()
text = buf.get_text(start, end, True)
if text == '':
buf.insert_with_tags(
start, self.PLACEHOLDER, self.placeholder_tag)
def remove_placeholder(self):
self._on_focus_in()
def after_paste_clipboard(self, textview):
buffer_ = textview.get_buffer()
......
......@@ -81,7 +81,6 @@ class MessageWindow(object):
self.xml = gtkgui_helpers.get_gtk_builder('%s.ui' % self.widget_name)
self.window = self.xml.get_object(self.widget_name)
self.window.set_application(app.app)
self.window.set_show_menubar(False)
self.notebook = self.xml.get_object('notebook')
self.parent_paned = None
......@@ -94,17 +93,26 @@ class MessageWindow(object):
if app.config.get('roster_on_the_right'):
child1 = self.parent_paned.get_child1()
self.parent_paned.remove(child1)
self.parent_paned.add(self.notebook)
self.parent_paned.pack1(self.notebook, resize=False,
shrink=True)
self.parent_paned.pack2(child1, resize=True, shrink=True)
self.parent_paned.pack1(self.notebook, resize=False)
self.parent_paned.pack2(child1)
else:
self.parent_paned.add(self.notebook)
self.parent_paned.pack2(self.notebook, resize=True, shrink=True)
self.parent_paned.pack2(self.notebook)
self.window.lookup_action('show-roster').set_enabled(True)
orig_window.destroy()
del orig_window
# Set headermenu
# single-window mode: show the header menu on the roster window
# all other modes: add the headerbar to the new window
# A headerbar has to be set before the window calls show()
if parent_window:
self.header_menu = app.interface.roster.header_menu
self.header_menu.show()
else:
self.header_menu = self.xml.get_object('header_menu')
headerbar = self.xml.get_object('headerbar')
self.window.set_titlebar(headerbar)
# NOTE: we use 'connect_after' here because in
# MessageWindowMgr._new_window we register handler that saves window
# state when closing it, and it should be called before
......@@ -162,6 +170,9 @@ class MessageWindow(object):
self.notebook.set_show_border(app.config.get('tabs_border'))
self.show_icon()
def set_header_menu(self, menu):
self.header_menu.set_menu_model(menu)
def change_account_name(self, old_name, new_name):
if old_name in self._controls:
self._controls[new_name] = self._controls[old_name]
......@@ -324,6 +335,7 @@ class MessageWindow(object):
self.notebook.show_all()
else:
self.window.show_all()
# NOTE: we do not call set_control_active(True) since we don't know
# whether the tab is the active one.
self.show_title()
......@@ -436,9 +448,6 @@ class MessageWindow(object):
elif chr(keyval) in st: # ALT + 1,2,3..
self.notebook.set_current_page(st.index(chr(keyval)))
return True
elif keyval == Gdk.KEY_c: # ALT + C toggles chat buttons
control.chat_buttons_set_visible(not control.hide_chat_buttons)
return True
elif keyval == Gdk.KEY_m: # ALT + M show emoticons menu
control.emoticons_button.get_popover().show()
return True
......@@ -570,6 +579,7 @@ class MessageWindow(object):
ask any confirmation
"""
def close(ctrl):
self.remove_headermenu(self.notebook, ctrl)
if reason is not None: # We are leaving gc with a status message
ctrl.shutdown(reason)
else: # We are leaving gc without status message or it's a chat
......@@ -607,6 +617,7 @@ class MessageWindow(object):
def on_minimize(ctrl):
if method != self.CLOSE_COMMAND:
self.remove_headermenu(self.notebook, ctrl)
ctrl.minimize()
self.check_tabs()
return
......@@ -618,6 +629,13 @@ class MessageWindow(object):
else:
ctrl.allow_shutdown(method, on_yes, on_no, on_minimize)
def remove_headermenu(self, notebook, ctrl):
page_num = notebook.page_num(ctrl.widget)
if page_num == notebook.get_current_page():
self.set_header_menu(None)
elif notebook.get_n_pages()