changeset 83287ed2d759 in tryton:default
details: https://hg.tryton.org/tryton?cmd=changeset;node=83287ed2d759
description:
Send client email via server
issue9449
review292001002
diffstat:
CHANGELOG | 2 +
doc/usage.rst | 26 +-
tryton/action/main.py | 43 +-
tryton/client.py | 2 +-
tryton/common/popup_menu.py | 24 +-
tryton/common/richtext.py | 346 +++++++++++++
tryton/data/pixmaps/tryton/tryton-send.svg | 1 +
tryton/gui/main.py | 24 +-
tryton/gui/window/email_.py | 371 +++++++++++---
tryton/gui/window/form.py | 38 +-
tryton/gui/window/tabcontent.py | 11 +-
tryton/gui/window/view_form/view/form_gtk/richtextbox.py | 314 +------------
12 files changed, 726 insertions(+), 476 deletions(-)
diffs (1549 lines):
diff -r 5ce1d6984748 -r 83287ed2d759 CHANGELOG
--- a/CHANGELOG Sat Sep 05 22:59:38 2020 +0200
+++ b/CHANGELOG Thu Sep 17 17:18:02 2020 +0200
@@ -1,3 +1,5 @@
+* Support e-mail template
+* Send e-mail via the server
* Support PYSON comparison of date and datetime
* Position copied records based on order
* Allow configuration of default colors for graph and calendar
diff -r 5ce1d6984748 -r 83287ed2d759 doc/usage.rst
--- a/doc/usage.rst Sat Sep 05 22:59:38 2020 +0200
+++ b/doc/usage.rst Thu Sep 17 17:18:02 2020 +0200
@@ -197,30 +197,6 @@
Search Limit:
Open a dialog to set up the maximum number of records displayed on a list.
-.. _Menu-Options-Email:
-
-Email:
- Open a dialog to set up an email reader.
-
- * Command Line: The command line calling the email reader.
- * Placeholders:
-
- - ``${to}``: the destination email address
- - ``${cc}``: the carbon copy email address
- - ``${subject}``: the subject of the email
- - ``${body}``: the body of the email
- - ``${attachment}``: the attachment of the email
-
- * Examples:
-
- - Thunderbird 2 on Linux:
- ``thunderbird -compose
"to='${to}',cc='${cc}',subject='${subject}',body='${body}',attachment='file://${attachment}'"``
-
- - Thunderbird 2 on Windows XP SP3:
- ``"C:\\Program Files\\Mozilla Thunderbird\\thunderbird.exe" -compose
to="${to}",cc="${cc}",subject="${subject}",body="${body}",attachment="${attachment}"``
-
-.. note:: The path of *Program Files* may vary dependent on the localization
of your Windows version.
-
.. _Menu-Options-Check_Version:
Check Version:
@@ -343,7 +319,7 @@
.. _Toolbar-Email:
E-Mail...:
- Shows all email reports for the actual view, model and record.
+ Open an editor to send an email related to the actual record.
.. _Toolbar-Print:
diff -r 5ce1d6984748 -r 83287ed2d759 tryton/action/main.py
--- a/tryton/action/main.py Sat Sep 05 22:59:38 2020 +0200
+++ b/tryton/action/main.py Thu Sep 17 17:18:02 2020 +0200
@@ -5,7 +5,7 @@
import tryton.rpc as rpc
from tryton.common import RPCProgress, RPCExecute, RPCException
-from tryton.common import message, selection, file_write, file_open, mailto
+from tryton.common import message, selection, file_write, file_open
from tryton.config import CONFIG
from tryton.pyson import PYSONDecoder
@@ -15,18 +15,13 @@
class Action(object):
@staticmethod
- def exec_report(name, data, direct_print=False, email_print=False,
- email=None, context=None):
+ def exec_report(name, data, direct_print=False, context=None):
if context is None:
context = {}
- if email is None:
- email = {}
data = data.copy()
ctx = rpc.CONTEXT.copy()
ctx.update(context)
ctx['direct_print'] = direct_print
- ctx['email_print'] = email_print
- ctx['email'] = email
args = ('report', name, 'execute', data.get('ids', []), data, ctx)
try:
res = RPCProgress('execute', args).run()
@@ -39,12 +34,7 @@
print_p = True
fp_name = file_write((name, type), data)
- if email_print:
- mailto(to=email.get('to'), cc=email.get('cc'),
- subject=email.get('subject'), body=email.get('body'),
- attachment=fp_name)
- else:
- file_open(fp_name, type, print_p=print_p)
+ file_open(fp_name, type, print_p=print_p)
return True
@staticmethod
@@ -159,16 +149,15 @@
name = add_name_suffix(name, context)
Window.create_wizard(action['wiz_name'], data,
direct_print=action.get('direct_print', False),
- email_print=action.get('email_print', False),
- email=action.get('email'), name=name,
+ name=name,
context=context, icon=(action.get('icon.rec_name') or ''),
window=action.get('window', False))
elif action['type'] == 'ir.action.report':
- Action.exec_report(action['report_name'], data,
- direct_print=action.get('direct_print', False),
- email_print=action.get('email_print', False),
- email=action.get('email'), context=context)
+ Action.exec_report(
+ action['report_name'], data,
+ direct_print=action.get('direct_print', False),
+ context=context)
elif action['type'] == 'ir.action.url':
if action['url']:
@@ -197,19 +186,3 @@
elif not len(keyact) and warning:
message(_('No action defined.'))
return False
-
- @staticmethod
- def evaluate(action, atype, record):
- '''
- Evaluate the action with the record.
- '''
- action = action.copy()
- email = {}
- if 'pyson_email' in action:
- email = record.expr_eval(action['pyson_email'])
- if not email:
- email = {}
- if 'subject' not in email:
- email['subject'] = action['name'].replace('_', '')
- action['email'] = email
- return action
diff -r 5ce1d6984748 -r 83287ed2d759 tryton/client.py
--- a/tryton/client.py Sat Sep 05 22:59:38 2020 +0200
+++ b/tryton/client.py Thu Sep 17 17:18:02 2020 +0200
@@ -20,7 +20,7 @@
.readonly entry, .readonly text {
background-color: @insensitive_bg_color;
}
- .required entry, .required text {
+ .required entry, entry.required, .required text, text.required {
border-color: darker(@unfocused_borders);
}
.invalid entry, entry.invalid, .invalid text, text.invalid {
diff -r 5ce1d6984748 -r 83287ed2d759 tryton/common/popup_menu.py
--- a/tryton/common/popup_menu.py Sat Sep 05 22:59:38 2020 +0200
+++ b/tryton/common/popup_menu.py Thu Sep 17 17:18:02 2020 +0200
@@ -5,10 +5,12 @@
from gi.repository import Gtk, Gdk
from tryton.common import RPCExecute, RPCException
+from tryton.common.common import selection
from tryton.gui.window.view_form.screen import Screen
from tryton.action import Action
from tryton.gui.window import Window
from tryton.gui.window.attachment import Attachment
+from tryton.gui.window.email_ import Email
from tryton.gui.window.note import Note
_ = gettext.gettext
@@ -41,7 +43,6 @@
def activate(menuitem, action, atype):
rec = load(record)
- action = Action.evaluate(action, atype, rec)
data = {
'model': model,
'id': rec.id,
@@ -61,6 +62,20 @@
def note(menuitem):
Note(load(record), None)
+ def is_report(action):
+ return action['type'] == 'ir.action.report'
+
+ def email(menuitem, toolbar):
+ rec = load(record)
+ prints = filter(is_report, toolbar['print'])
+ emails = {e['name']: e['id'] for e in toolbar['emails']}
+ template = selection(_("Template"), emails, alwaysask=True)
+ if template:
+ template = template[1]
+ Email(
+ '%s: %s' % (title, rec.rec_name()), rec, prints,
+ template=template)
+
def edit(menuitem):
with Window(hide_current=True, allow_similar=True):
Window.create(model,
@@ -103,7 +118,6 @@
('action', 'tryton-launch', _('Actions...'), None),
('relate', 'tryton-link', _('Relate...'), None),
('print', 'tryton-open', _('Report...'), 'open'),
- ('print', 'tryton-email', _('E-Mail...'), 'email'),
('print', 'tryton-print', _('Print...'), 'print'),
):
if len(action_menu):
@@ -121,8 +135,10 @@
submenu.append(item)
if flavor == 'print':
action['direct_print'] = True
- elif flavor == 'email':
- action['email_print'] = True
item.connect('activate', activate, action, atype)
menu.show_all()
+
+ email_item = Gtk.MenuItem(label=_('E-Mail...'))
+ action_menu.append(email_item)
+ email_item.connect('activate', email, toolbar)
menu.show_all()
diff -r 5ce1d6984748 -r 83287ed2d759 tryton/common/richtext.py
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/tryton/common/richtext.py Thu Sep 17 17:18:02 2020 +0200
@@ -0,0 +1,346 @@
+# This file is part of Tryton. The COPYRIGHT file at the top level of
+# this repository contains the full copyright notices and license terms.
+import gettext
+from contextlib import contextmanager
+
+from gi.repository import Gtk, Gdk
+
+from tryton.common import IconFactory
+from tryton.common.htmltextbuffer import (
+ serialize, deserialize,
+ setup_tags, register_foreground, remove_tags,
+ FAMILIES, SIZE2SCALE, MIME, use_serialize_func)
+
+_ = gettext.gettext
+SIZES = sorted(SIZE2SCALE.keys())
+
+
+def register_format(textview):
+ buffer_ = textview.get_buffer()
+ setup_tags(buffer_)
+ buffer_.register_serialize_format(str(MIME), serialize, None)
+ buffer_.register_deserialize_format(str(MIME), deserialize, None)
+
+
+def set_content(textview, content):
+ with disable_text_style(textview):
+ buffer_ = textview.get_buffer()
+ start = buffer_.get_start_iter()
+ end = buffer_.get_end_iter()
+ buffer_.delete(start, end)
+ if use_serialize_func:
+ buffer_.deserialize(buffer_, MIME, start, content)
+ else:
+ deserialize(
+ buffer_, buffer_, start, content,
+ buffer_.deserialize_get_can_create_tags(MIME), None)
+
+
+def get_content(textview):
+ buffer_ = textview.get_buffer()
+ start = buffer_.get_start_iter()
+ end = buffer_.get_end_iter()
+ if use_serialize_func:
+ return buffer_.serialize(buffer_, MIME, start, end)
+ else:
+ return serialize(buffer_, buffer_, start, end, None)
+
+
+def add_toolbar(textview):
+ toolbar = Gtk.Toolbar()
+
+ tag_widgets = {}
+ colors = {}
+
+ for icon, label in [
+ ('bold', _("Bold")),
+ ('italic', _("Italic")),
+ ('underline', _("Underline")),
+ ]:
+ button = Gtk.ToggleToolButton()
+ button.set_icon_widget(IconFactory.get_image(
+ 'tryton-format-%s' % icon,
+ Gtk.IconSize.SMALL_TOOLBAR))
+ button.set_label(label)
+ button.connect('toggled', _toggle_props, icon, textview)
+ toolbar.insert(button, -1)
+ tag_widgets[icon] = button
+
+ toolbar.insert(Gtk.SeparatorToolItem(), -1)
+
+ for name, options, active in [
+ ('family', FAMILIES, FAMILIES.index('normal')),
+ ('size', SIZES, SIZES.index('4')),
+ ]:
+ combobox = Gtk.ComboBoxText()
+ for option in options:
+ combobox.append_text(option)
+ combobox.set_active(active)
+ combobox.set_focus_on_click(False)
+ combobox.connect('changed', _change_props, name, textview)
+ tool = Gtk.ToolItem()
+ tool.add(combobox)
+ toolbar.insert(tool, -1)
+ tag_widgets[name] = combobox
+
+ toolbar.insert(Gtk.SeparatorToolItem(), -1)
+
+ button = None
+ for name, label in [
+ ('left', _("Align Left")),
+ ('center', _("Align Center")),
+ ('right', _("Align Right")),
+ ('justify', _("Justify")),
+ ]:
+ icon = 'tryton-format-align-%s' % name
+ button = Gtk.RadioToolButton.new_from_widget(button)
+ button.set_icon_widget(IconFactory.get_image(
+ icon, Gtk.IconSize.SMALL_TOOLBAR))
+ button.set_active(icon == 'left')
+ button.set_label(label)
+ button.connect(
+ 'toggled', _toggle_justification, name, textview)
+ toolbar.insert(button, -1)
+ tag_widgets[name] = button
+
+ toolbar.insert(Gtk.SeparatorToolItem(), -1)
+
+ for icon, label in [
+ ('foreground', _("Foreground Color")),
+ # TODO ('background', _('Background')),
+ ]:
+ button = Gtk.ToolButton()
+ if icon == 'foreground':
+ button.set_icon_widget(IconFactory.get_image(
+ 'tryton-format-color-text',
+ Gtk.IconSize.SMALL_TOOLBAR))
+ button.set_label(label)
+ button.connect('clicked', _toggle_color, icon, textview, colors)
+ toolbar.insert(button, -1)
+ tag_widgets[icon] = button
+
+ buffer_ = textview.get_buffer()
+ buffer_.connect_after(
+ 'insert-text', _insert_text_style, tag_widgets)
+ textview.connect_after(
+ 'move-cursor',
+ lambda *a: _detect_style(textview, tag_widgets, colors))
+ textview.connect_after(
+ 'button-release-event',
+ lambda *a: _detect_style(textview, tag_widgets, colors))
+
+ return toolbar
+
+
+@contextmanager
+def disable_text_style(textview):
+ buffer_ = textview.get_buffer()
+ try:
+ buffer_.handler_block_by_func(_insert_text_style)
+ except TypeError:
+ pass
+ yield
+ try:
+ buffer_.handler_unblock_by_func(_insert_text_style)
+ except TypeError:
+ pass
+
+
+def _toggle_props(toggle, name, textview):
+ buffer_ = textview.get_buffer()
+ try:
+ start, end = buffer_.get_selection_bounds()
+ except ValueError:
+ return
+ _apply_tool(buffer_, name, toggle, start, end)
+
+
+def _change_props(combobox, name, textview):
+ buffer_ = textview.get_buffer()
+ try:
+ start, end = buffer_.get_selection_bounds()
+ except ValueError:
+ return
+ _apply_tool(buffer_, name, combobox, start, end)
+
+
+def _toggle_justification(button, name, textview):
+ buffer_ = textview.get_buffer()
+ try:
+ start, end = buffer_.get_selection_bounds()
+ except ValueError:
+ insert = buffer_.get_insert()
+ start = buffer_.get_iter_at_mark(insert)
+ end = start.copy()
+ start.set_line_offset(0)
+ if not end.ends_line():
+ end.forward_to_line_end()
+ _apply_tool(buffer_, name, button, start, end)
+
+
+def _toggle_color(button, name, textview, colors):
+ buffer_ = textview.get_buffer()
+ insert = buffer_.get_insert()
+ try:
+ start, end = buffer_.get_selection_bounds()
+ except ValueError:
+ start = end = None
+ else:
+ # Use offset position to preserve across buffer_ modification
+ start = start.get_offset()
+ end = end.get_offset()
+
+ dialog = Gtk.ColorChooserDialog(
+ title=_('Select a color'),
+ transient_for=textview.get_toplevel(),
+ use_alpha=False)
+ color = Gdk.RGBA()
+ if name in colors:
+ color.parse(colors[name])
+ dialog.set_rgba(color)
+ if dialog.run() == Gtk.ResponseType.OK:
+ color = dialog.get_rgba()
+ if start is not None and end is not None:
+ start = buffer_.get_iter_at_offset(start)
+ end = buffer_.get_iter_at_offset(end)
+ tag = register_foreground(buffer_, color)
+ remove_tags(buffer_, start, end, name)
+ buffer_.apply_tag(tag, start, end)
+ dialog.destroy()
+ buffer_.place_cursor(buffer_.get_iter_at_mark(insert))
+
+
+def _apply_tool(buffer_, name, tool, start, end):
+ # First test RadioToolButton as they inherit from ToggleToolButton
+ if isinstance(tool, Gtk.RadioToolButton):
+ name = 'justification %s' % name
+ if not tool.get_active():
+ remove_tags(buffer_, start, end, name)
+ else:
+ remove_tags(buffer_, start, end, 'justification')
+ buffer_.apply_tag_by_name(name, start, end)
+ elif isinstance(tool, Gtk.ToggleToolButton):
+ if tool.get_active():
+ buffer_.apply_tag_by_name(name, start, end)
+ else:
+ buffer_.remove_tag_by_name(name, start, end)
+ elif isinstance(tool, Gtk.ComboBoxText):
+ value = tool.get_active_text()
+ remove_tags(buffer_, start, end, name)
+ name = '%s %s' % (name, value)
+ buffer_.apply_tag_by_name(name, start, end)
+
+
+def _insert_text_style(buffer_, iter_, text, length, tag_widgets):
+ # Text is already inserted so iter_ points to the end
+ start = iter_.copy()
+ start.backward_chars(length)
+ end = iter_.copy()
+ # Apply tags activated from the toolbar
+ for name, widget in tag_widgets.items():
+ _apply_tool(buffer_, name, widget, start, end)
+
+
+def _detect_style(textview, tag_widgets, colors):
+ buffer_ = textview.get_buffer()
+ try:
+ start, end = buffer_.get_selection_bounds()
+ except ValueError:
+ start = end = buffer_.get_iter_at_mark(
+ buffer_.get_insert())
+
+ def toggle_button(name, values):
+ try:
+ value, = values
+ except ValueError:
+ value = False
+ button = tag_widgets[name]
+ button.handler_block_by_func(_toggle_props)
+ button.set_active(value)
+ button.handler_unblock_by_func(_toggle_props)
+
+ def set_combobox(name, indexes):
+ try:
+ index, = indexes
+ except ValueError:
+ index = -1
+ combobox = tag_widgets[name]
+ combobox.handler_block_by_func(_change_props)
+ combobox.set_active(index)
+ combobox.handler_unblock_by_func(_change_props)
+
+ def toggle_justification(names, value):
+ if len(names) != 1:
+ value = False
+ for name in names:
+ button = tag_widgets[name]
+ button.handler_block_by_func(_toggle_justification)
+ button.set_active(value)
+ button.handler_unblock_by_func(_toggle_justification)
+
+ bolds, italics, underlines = set(), set(), set()
+ families, sizes, justifications = set(), set(), set()
+ colors['foreground'] = 'black'
+
+ iter_ = start.copy()
+ while True:
+ bold, italic, underline = False, False, False
+ family = FAMILIES.index('normal')
+ size = SIZES.index('4')
+ justification = 'left'
+
+ for tag in iter_.get_tags():
+ if not tag.props.name:
+ continue
+ elif tag.props.name == 'bold':
+ bold = True
+ elif tag.props.name == 'italic':
+ italic = True
+ elif tag.props.name == 'underline':
+ underline = True
+ elif tag.props.name.startswith('family'):
+ _, family = tag.props.name.split()
+ family = FAMILIES.index(family)
+ elif tag.props.name.startswith('size'):
+ _, size = tag.props.name.split()
+ size = SIZES.index(size)
+ elif tag.props.name.startswith('justification'):
+ _, justification = tag.props.name.split()
+ elif tag.props.name.startswith('foreground'):
+ _, colors['foreground'] = tag.props.name.split()
+ bolds.add(bold)
+ italics.add(italic)
+ underlines.add(underline)
+ families.add(family)
+ sizes.add(size)
+ justifications.add(justification)
+
+ iter_.forward_char()
+ if iter_.compare(end) > 0:
+ iter_ = end
+ if iter_.compare(end) == 0:
+ break
+
+ for name, values in [
+ ('bold', bolds),
+ ('italic', italics),
+ ('underline', underlines)]:
+ toggle_button(name, values)
+ set_combobox('family', families)
+ set_combobox('size', sizes)
+ toggle_justification(justifications, True)
+
+
+if __name__ == '__main__':
+ win = Gtk.Window()
+ box = Gtk.VBox()
+ win.add(box)
+
+ textview = Gtk.TextView()
+ register_format(textview)
+ toolbar = add_toolbar(textview)
+ box.pack_start(toolbar, expand=False, fill=True, padding=0)
+ box.pack_start(textview, expand=True, fill=True, padding=0)
+
+ win.show_all()
+ Gtk.main()
diff -r 5ce1d6984748 -r 83287ed2d759 tryton/data/pixmaps/tryton/tryton-send.svg
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/tryton/data/pixmaps/tryton/tryton-send.svg Thu Sep 17 17:18:02
2020 +0200
@@ -0,0 +1,1 @@
+<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 0 24 24"
width="24"><path d="M0 0h24v24H0z" fill="none"/><path d="M2.01 21L23 12 2.01 3
2 10l15 2-15 2z"/></svg>
\ No newline at end of file
diff -r 5ce1d6984748 -r 83287ed2d759 tryton/gui/main.py
--- a/tryton/gui/main.py Sat Sep 05 22:59:38 2020 +0200
+++ b/tryton/gui/main.py Thu Sep 17 17:18:02 2020 +0200
@@ -100,10 +100,6 @@
action.connect('activate', lambda *a: self.edit_limit())
self.add_action(action)
- action = Gio.SimpleAction.new('email', None)
- action.connect('activate', lambda *a: self.edit_email())
- self.add_action(action)
-
self._shortcuts = None
action = Gio.SimpleAction.new('shortcuts', None)
action.connect('activate', lambda *a: self.shortcuts())
@@ -198,6 +194,9 @@
Gtk.AccelMap.add_entry(
'<tryton>/Form/Report', Gdk.KEY_P, Gdk.ModifierType.CONTROL_MASK)
Gtk.AccelMap.add_entry(
+ '<tryton>/Form/Email', Gdk.KEY_E,
+ Gdk.ModifierType.CONTROL_MASK | Gdk.ModifierType.SHIFT_MASK)
+ Gtk.AccelMap.add_entry(
'<tryton>/Form/Search', Gdk.KEY_F, Gdk.ModifierType.CONTROL_MASK)
Gtk.AccelMap.load(os.path.join(get_config_dir(), 'accel.map'))
@@ -296,7 +295,6 @@
section.append(_("PDA Mode"), 'app.mode-pda')
section.append(_("Search Limit..."), 'app.search-limit')
- section.append(_("Email..."), 'app.email')
section.append(_("Check Version"), 'app.check-version')
menu.append_section(_("Options"), section)
@@ -491,10 +489,6 @@
from tryton.gui.window.limit import Limit
Limit().run()
- def edit_email(self):
- from tryton.gui.window.email_ import Email
- Email().run()
-
def win_next(self):
page = self.notebook.get_current_page()
if page == len(self.pages) - 1:
@@ -1010,8 +1004,6 @@
data = json.loads(params.get('data', '{}'),
object_hook=object_hook)
direct_print = json.loads(params.get('direct_print', 'false'))
- email_print = json.loads(params.get('email_print', 'false'))
- email = json.loads(params.get('email', 'null'))
name = json.loads(params.get('name', '""'))
window = json.loads(params.get('window', 'false'))
context = json.loads(params.get('context', '{}'),
@@ -1019,8 +1011,8 @@
except ValueError:
return
try:
- Window.create_wizard(wizard, data, direct_print=direct_print,
- email_print=email_print, email=email, name=name,
+ Window.create_wizard(
+ wizard, data, direct_print=direct_print, name=name,
context=context, window=window)
except Exception:
# Prevent crashing the client
@@ -1032,15 +1024,13 @@
try:
data = json.loads(params.get('data'), object_hook=object_hook)
direct_print = json.loads(params.get('direct_print', 'false'))
- email_print = json.loads(params.get('email_print', 'false'))
- email = json.loads(params.get('email', 'null'))
context = json.loads(params.get('context', '{}'),
object_hook=object_hook)
except ValueError:
return
try:
- Action.exec_report(report, data, direct_print=direct_print,
- email_print=email_print, email=email, context=context)
+ Action.exec_report(
+ report, data, direct_print=direct_print, context=context)
except Exception:
# Prevent crashing the client
return
diff -r 5ce1d6984748 -r 83287ed2d759 tryton/gui/window/email_.py
--- a/tryton/gui/window/email_.py Sat Sep 05 22:59:38 2020 +0200
+++ b/tryton/gui/window/email_.py Thu Sep 17 17:18:02 2020 +0200
@@ -1,97 +1,310 @@
# This file is part of Tryton. The COPYRIGHT file at the top level of
# this repository contains the full copyright notices and license terms.
+import gettext
+import logging
+import os
-import gettext
-
-from gi.repository import Gtk
+from gi.repository import Gtk, GLib
+try:
+ from gi.repository import GtkSpell
+except ImportError:
+ GtkSpell = None
-from tryton.common import get_toplevel_window, IconFactory
+from tryton.common import IconFactory, Tooltips, RPCExecute, RPCException
+from tryton.common.richtext import (
+ register_format, add_toolbar, get_content, set_content)
from tryton.common.underline import set_underline
+from tryton.common.widget_style import widget_class
from tryton.config import TRYTON_ICON, CONFIG
+from tryton.exceptions import TrytonServerError, TrytonError
from tryton.gui import Main
+from tryton.gui.window.nomodal import NoModal
_ = gettext.gettext
+logger = logging.getLogger(__name__)
+
+
+class EmailEntry(Gtk.Entry):
+ def __init__(self, *args, **kwargs):
+ super().__init__(*args, **kwargs)
+ self._completion = Gtk.EntryCompletion()
+ self._completion.set_model(Gtk.ListStore(str, str))
+ self._completion.set_text_column(0)
+ self._completion.set_match_func(lambda *a: True)
+ self._completion.connect('match-selected', self._match_selected)
+ self.set_completion(self._completion)
+ self.connect('changed', EmailEntry._changed)
+
+ def _match_selected(self, completion, model, iter):
+ self.set_text(model.get_value(iter, 1))
+ self.set_position(-1)
+ return True
+
+ def _update_completion(self, text):
+ if not self.props.window:
+ return False
+ if text != self.get_text():
+ return False
+ model = self._completion.get_model()
+ if not text:
+ model.clear()
+ model.text = text
+ return False
+ if getattr(model, 'text', None) == text:
+ return False
+
+ def callback(results):
+ try:
+ results = results()
+ except (TrytonError, TrytonServerError):
+ logger.warning(
+ _("Unable to complete email entry"), exc_info=True)
+ results = []
+ if text != self.get_text():
+ return False
+ model.clear()
+ for ratio, address, addresses in results:
+ model.append([address, addresses])
+ model.text = text
+ # Force display of popup
+ self.emit('changed')
+
+ try:
+ RPCExecute(
+ 'model', 'ir.email', 'complete', text, CONFIG['client.limit'],
+ process_exception=False, callback=callback)
+ except Exception:
+ logger.warning(
+ _("Unable to complete email entry"), exc_info=True)
+ return False
+
+ def _changed(self):
+ def keypress():
+ if not self.props.window:
+ return
+ self._update_completion()
+ text = self.get_text()
+ if self.get_position() >= len(text) - 1:
+ GLib.timeout_add(300, self._update_completion, text)
+ else:
+ model = self._completion.get_model()
+ model.clear()
+ model.text = None
-class Email(object):
+class Email(NoModal):
+
+ def __init__(self, name, record, prints, template=None):
+ super().__init__()
+ self.record = record
+ self.dialog = Gtk.Dialog(
+ transient_for=self.parent, destroy_with_parent=True)
+ Main().add_window(self.dialog)
+ self.dialog.set_position(Gtk.WindowPosition.CENTER_ON_PARENT)
+ self.dialog.set_icon(TRYTON_ICON)
+ self.dialog.set_default_size(*self.default_size())
+ self.dialog.connect('response', self.response)
+
+ self.dialog.set_title(_('E-mail %s') % name)
+
+ grid = Gtk.Grid(
+ column_spacing=3, row_spacing=3,
+ border_width=3)
+ self.dialog.vbox.pack_start(grid, expand=True, fill=True, padding=0)
- def __init__(self):
- self.parent = get_toplevel_window()
- self.win = Gtk.Dialog(
- title=_('Email'), transient_for=self.parent, modal=True,
- destroy_with_parent=True)
- Main().add_window(self.win)
- cancel_button = self.win.add_button(
- set_underline(_("Cancel")), Gtk.ResponseType.CANCEL)
- cancel_button.set_image(
- IconFactory.get_image('tryton-cancel', Gtk.IconSize.BUTTON))
- cancel_button.set_always_show_image(True)
- ok_button = self.win.add_button(
- set_underline(_("OK")), Gtk.ResponseType.OK)
- ok_button.set_image(
- IconFactory.get_image('tryton-ok', Gtk.IconSize.BUTTON))
- ok_button.set_always_show_image(True)
- self.win.set_default_response(Gtk.ResponseType.OK)
- self.win.set_icon(TRYTON_ICON)
- self.win.vbox.set_spacing(3)
- self.win.vbox.pack_start(Gtk.Label(
- _('Email Program Settings')), expand=False, fill=True, padding=0)
- self.win.vbox.pack_start(
- Gtk.HSeparator(), expand=True, fill=True, padding=0)
- hbox = Gtk.HBox(spacing=3)
- label = Gtk.Label(label=_('Command Line:'))
- hbox.pack_start(label, expand=True, fill=True, padding=0)
- self.entry = Gtk.Entry()
- self.entry.set_property('activates_default', True)
- self.entry.set_width_chars(50)
- self.entry.set_text(CONFIG['client.email'])
- label.set_mnemonic_widget(label)
- hbox.pack_start(self.entry, expand=True, fill=True, padding=0)
- self.win.vbox.pack_start(hbox, expand=True, fill=True, padding=0)
+ label = Gtk.Label(
+ set_underline(_("To:")), use_underline=True, halign=Gtk.Align.END)
+ grid.attach(label, 0, 0, 1, 1)
+ self.to = EmailEntry(hexpand=True, activates_default=True)
+ widget_class(self.to, 'required', True)
+ label.set_mnemonic_widget(self.to)
+ grid.attach(self.to, 1, 0, 1, 1)
+
+ label = Gtk.Label(
+ set_underline(_("Cc:")), use_underline=True, halign=Gtk.Align.END)
+ grid.attach(label, 0, 1, 1, 1)
+ self.cc = EmailEntry(hexpand=True, activates_default=True)
+ label.set_mnemonic_widget(self.cc)
+ grid.attach(self.cc, 1, 1, 1, 1)
+
+ label = Gtk.Label(
+ set_underline(_("Bcc:")), use_underline=True, halign=Gtk.Align.END)
+ grid.attach(label, 0, 2, 1, 1)
+ self.bcc = EmailEntry(hexpand=True, activates_default=True)
+ label.set_mnemonic_widget(self.bcc)
+ grid.attach(self.bcc, 1, 2, 1, 1)
label = Gtk.Label(
- label=_('Legend of Available Placeholders:'),
- halign=Gtk.Align.START, margin_top=10, margin_bottom=5)
- self.win.vbox.pack_start(label, expand=False, fill=True, padding=0)
+ set_underline(_("Subject:")),
+ use_underline=True, halign=Gtk.Align.END)
+ grid.attach(label, 0, 3, 1, 1)
+ self.subject = Gtk.Entry(hexpand=True, activates_default=True)
+ label.set_mnemonic_widget(self.subject)
+ grid.attach(self.subject, 1, 3, 1, 1)
+
+ self.body = Gtk.TextView()
+ body_frame = Gtk.Frame()
+ label = Gtk.Label(
+ set_underline(_("Body")), use_underline=True, halign=Gtk.Align.END)
+ label.set_mnemonic_widget(self.body)
+ body_frame.set_label_widget(label)
+ grid.attach(body_frame, 0, 4, 2, 1)
+ body_box = Gtk.VBox(hexpand=True, vexpand=True)
+ body_frame.add(body_box)
+ register_format(self.body)
+ body_toolbar = add_toolbar(self.body)
+ body_box.pack_start(body_toolbar, expand=False, fill=True, padding=0)
+ body_box.pack_start(self.body, expand=True, fill=True, padding=0)
+
+ if GtkSpell and CONFIG['client.spellcheck']:
+ checker = GtkSpell.Checker()
+ checker.attach(self.body)
+ language = os.environ.get('LANGUAGE', 'en')
+ try:
+ checker.set_language(language)
+ except Exception:
+ logger.error(
+ 'Could not set spell checker for "%s"', language)
+ checker.detach()
+
+ attachments = Gtk.HBox()
+ grid.attach(attachments, 0, 5, 2, 1)
- hbox = Gtk.HBox(spacing=3)
- vboxl = Gtk.VBox(homogeneous=True, spacing=3)
- label = Gtk.Label(label=_('To:'), halign=Gtk.Align.START)
- vboxl.pack_start(label, expand=False, fill=False, padding=0)
- label = Gtk.Label(label=_('CC:'), halign=Gtk.Align.START)
- vboxl.pack_start(label, expand=False, fill=False, padding=0)
- label = Gtk.Label(label=_('Subject:'), halign=Gtk.Align.START)
- vboxl.pack_start(label, expand=False, fill=False, padding=0)
- label = Gtk.Label(label=_('Body:'), halign=Gtk.Align.START)
- vboxl.pack_start(label, expand=False, fill=False, padding=0)
- label = Gtk.Label(label=_('Attachment:'), halign=Gtk.Align.START)
- vboxl.pack_start(label, expand=False, fill=False, padding=0)
+ print_frame = Gtk.Frame(shadow_type=Gtk.ShadowType.NONE)
+ print_frame.set_label(_("Reports"))
+ attachments.pack_start(print_frame, expand=True, fill=True, padding=0)
+ print_box = Gtk.VBox()
+ print_frame.add(print_box)
+ print_flowbox = Gtk.FlowBox(selection_mode=Gtk.SelectionMode.NONE)
+ print_box.pack_start(
+ print_flowbox, expand=False, fill=False, padding=0)
+ self.print_actions = {}
+ for print_ in prints:
+ print_check = Gtk.CheckButton.new_with_mnemonic(
+ set_underline(print_['name']))
+ self.print_actions[print_['id']] = print_check
+ print_flowbox.add(print_check)
+
+ file_frame = Gtk.Frame(shadow_type=Gtk.ShadowType.NONE)
+ file_frame.set_label(_("Attachments"))
+ attachments.pack_start(file_frame, expand=True, fill=True, padding=0)
+ self.files = Gtk.VBox(spacing=6)
+ file_frame.add(self.files)
+ self._add_file_button()
+
+ button_cancel = self.dialog.add_button(
+ set_underline(_("Cancel")), Gtk.ResponseType.CANCEL)
+ button_cancel.set_image(IconFactory.get_image(
+ 'tryton-cancel', Gtk.IconSize.BUTTON))
+
+ button_send = self.dialog.add_button(
+ set_underline(_("Send")), Gtk.ResponseType.OK)
+ button_send.set_image(IconFactory.get_image(
+ 'tryton-send', Gtk.IconSize.BUTTON))
+ self.dialog.set_default_response(Gtk.ResponseType.OK)
+
+ self._fill_with(template)
+
+ self.dialog.show_all()
+ self.register()
+
+ def _add_file_button(self):
+ tooltips = Tooltips()
+ box = Gtk.HBox(spacing=3)
+ self.files.pack_start(box, expand=False, fill=True, padding=0)
+ file_ = Gtk.FileChooserButton(title=_("Select Attachment"))
+ box.pack_start(file_, expand=True, fill=True, padding=0)
+ button = Gtk.Button()
+ button.set_image(IconFactory.get_image(
+ 'tryton-remove', Gtk.IconSize.BUTTON))
+ tooltips.set_tip(button, _("Remove attachment"))
+ button.set_sensitive(False)
+ box.pack_start(button, expand=False, fill=True, padding=0)
- vboxr = Gtk.VBox(homogeneous=True, spacing=3)
- label = Gtk.Label(label=' ${to}', halign=Gtk.Align.START)
- vboxr.pack_start(label, expand=False, fill=False, padding=0)
- label = Gtk.Label(label=' ${cc}', halign=Gtk.Align.START)
- vboxr.pack_start(label, expand=False, fill=False, padding=0)
- label = Gtk.Label(label=' ${subject}', halign=Gtk.Align.START)
- vboxr.pack_start(label, expand=False, fill=False, padding=0)
- label = Gtk.Label(label=' ${body}', halign=Gtk.Align.START)
- vboxr.pack_start(label, expand=False, fill=False, padding=0)
- label = Gtk.Label(label=' ${attachment}', halign=Gtk.Align.START)
- vboxr.pack_start(label, expand=False, fill=False, padding=0)
+ box.show_all()
+
+ file_.connect('file-set', self._file_set, button)
+ button.connect('clicked', self._file_remove)
+
+ def _file_set(self, file_, button):
+ button.set_sensitive(True)
+ self._add_file_button()
+
+ def _file_remove(self, button):
+ self.files.remove(button.get_parent())
+
+ def get_files(self):
+ for box in self.files:
+ file_ = list(box)[0]
+ filename = file_.get_filename()
+ if not filename:
+ continue
+ with open(filename, 'rb') as fp:
+ data = fp.read()
+ name = os.path.basename(filename)
+ yield (name, data)
+
+ def _fill_with(self, template=None):
+ try:
+ if template:
+ values = RPCExecute(
+ 'model', 'ir.email.template', 'get',
+ template, self.record.id)
+ else:
+ values = RPCExecute(
+ 'model', 'ir.email.template', 'get_default',
+ self.record.model_name, self.record.id)
+ except RPCException:
+ return
+ self.to.set_text(', '.join(values.get('to', [])))
+ self.cc.set_text(', '.join(values.get('cc', [])))
+ self.bcc.set_text(', '.join(values.get('bcc', [])))
+ self.subject.set_text(values.get('subject', ''))
+ set_content(self.body, values.get('body', ''))
+ print_ids = values.get('reports', [])
+ for print_id, print_check in self.print_actions.items():
+ print_check.set_active(print_id in print_ids)
- hbox.pack_start(vboxl, expand=False, fill=False, padding=0)
- hbox.pack_start(vboxr, expand=False, fill=False, padding=0)
-
- self.win.vbox.pack_start(hbox, expand=True, fill=True, padding=0)
-
- self.win.show_all()
+ def validate(self):
+ valid = True
+ if not self.subject.get_text():
+ valid = False
+ widget_class(self.subject, 'invalid', True)
+ self.subject.grab_focus()
+ else:
+ widget_class(self.subject, 'invalid', False)
+ if not self.to.get_text():
+ valid = False
+ widget_class(self.to, 'invalid', True)
+ self.to.grab_focus()
+ else:
+ widget_class(self.to, 'invalid', False)
+ return valid
- def run(self):
- "Run the window"
- res = self.win.run()
- if res == Gtk.ResponseType.OK:
- CONFIG['client.email'] = self.entry.get_text()
- CONFIG.save()
- self.parent.present()
- self.win.destroy()
- return res
+ def response(self, dialog, response):
+ if response == Gtk.ResponseType.OK:
+ if not self.validate():
+ return
+ to = self.to.get_text()
+ cc = self.cc.get_text()
+ bcc = self.bcc.get_text()
+ subject = self.subject.get_text()
+ body = get_content(self.body)
+ attachments = list(self.get_files())
+ reports = [
+ id_ for id_, check in self.print_actions.items()
+ if check.get_active()]
+ try:
+ RPCExecute(
+ 'model', 'ir.email', 'send',
+ to, cc, bcc, subject, body,
+ attachments,
+ [self.record.model_name, self.record.id],
+ reports)
+ except RPCException:
+ return
+ self.destroy()
+
+ def destroy(self):
+ super().destroy()
+ self.dialog.destroy()
diff -r 5ce1d6984748 -r 83287ed2d759 tryton/gui/window/form.py
--- a/tryton/gui/window/form.py Sat Sep 05 22:59:38 2020 +0200
+++ b/tryton/gui/window/form.py Thu Sep 17 17:18:02 2020 +0200
@@ -14,6 +14,7 @@
from tryton.action import Action
from tryton.gui import Main
from tryton.gui.window import Window
+from tryton.gui.window.email_ import Email
from tryton.gui.window.win_export import WinExport
from tryton.gui.window.win_import import WinImport
from tryton.gui.window.attachment import Attachment
@@ -23,6 +24,7 @@
from tryton.common import message, sur, sur_3b, timezoned_date
import tryton.common as common
from tryton.common import RPCExecute, RPCException
+from tryton.common.common import selection as selection_
from tryton.common.underline import set_underline
from tryton import plugins
@@ -437,9 +439,24 @@
if self.buttons['open'].props.sensitive:
self.buttons['open'].props.active = True
- def sig_print_email(self, widget):
+ def sig_email(self, widget):
+ def is_report(action):
+ return action['type'] == 'ir.action.report'
+
if self.buttons['email'].props.sensitive:
- self.buttons['email'].props.active = True
+ record = self.screen.current_record
+ if not record or record.id < 0:
+ return
+ toolbars = self.get_toolbars()
+ title = self.title.get_text()
+ prints = filter(is_report, toolbars['print'])
+ emails = {e['name']: e['id'] for e in toolbars['emails']}
+ template = selection_(_("Template"), emails, alwaysask=True)
+ if template:
+ template = template[1]
+ Email(
+ '%s: %s' % (title, record.rec_name()), record, prints,
+ template=template)
def sig_relate(self, widget):
if self.buttons['relate'].props.sensitive:
@@ -487,7 +504,7 @@
can_be_sensitive = getattr(button, '_can_be_sensitive', True)
if button_id in {'print', 'relate', 'email', 'open'}:
action_type = button_id
- if button_id in {'email', 'open'}:
+ if button_id == 'open':
action_type = 'print'
can_be_sensitive |= any(
b.attrs.get('keyword', 'action') == action_type
@@ -540,7 +557,6 @@
record_id = (self.screen.current_record.id
if self.screen.current_record else None)
record_ids = [r.id for r in self.screen.selected_records]
- action = Action.evaluate(action, atype, self.screen.current_record)
data = {
'model': self.screen.model_name,
'id': record_id,
@@ -567,11 +583,11 @@
attach_btn.connect('drag_data_received',
self.attach_drag_data_received)
+ pos = gtktoolbar.get_item_index(self.buttons['email'])
iconstock = {
'print': 'tryton-print',
'action': 'tryton-launch',
'relate': 'tryton-link',
- 'email': 'tryton-email',
'open': 'tryton-open',
}
for action_type, special_action, action_name, tooltip in (
@@ -579,9 +595,8 @@
('relate', 'relate', _('Relate'), _('Open related records')),
(None,) * 4,
('print', 'open', _('Report'), _('Open report')),
- ('print', 'email', _('E-Mail'), _('E-Mail report')),
('print', 'print', _('Print'), _('Print report')),
- ):
+ ):
if action_type is not None:
tbutton = Gtk.ToggleToolButton()
tbutton.set_icon_widget(common.IconFactory.get_image(
@@ -598,7 +613,8 @@
tbutton._menu.get_children())
else:
tbutton = Gtk.SeparatorToolItem()
- gtktoolbar.insert(tbutton, -1)
+ gtktoolbar.insert(tbutton, pos)
+ pos += 1
exports = toolbars['exports']
if exports:
@@ -613,7 +629,9 @@
menuitem.connect('activate', self.do_export, export)
menu.add(menuitem)
- gtktoolbar.insert(Gtk.SeparatorToolItem(), -1)
+ last_item = gtktoolbar.get_nth_item(gtktoolbar.get_n_items() - 1)
+ if not isinstance(last_item, Gtk.SeparatorToolItem):
+ gtktoolbar.insert(Gtk.SeparatorToolItem(), -1)
url_button = Gtk.ToggleToolButton()
url_button.set_icon_widget(
@@ -644,8 +662,6 @@
new_action = action.copy()
if special_action == 'print':
new_action['direct_print'] = True
- elif special_action == 'email':
- new_action['email_print'] = True
menuitem = Gtk.MenuItem(label=set_underline(action['name']))
menuitem.set_use_underline(True)
menuitem.connect('activate', self._popup_menu_selected, widget,
diff -r 5ce1d6984748 -r 83287ed2d759 tryton/gui/window/tabcontent.py
--- a/tryton/gui/window/tabcontent.py Sat Sep 05 22:59:38 2020 +0200
+++ b/tryton/gui/window/tabcontent.py Thu Sep 17 17:18:02 2020 +0200
@@ -134,15 +134,16 @@
icon_name='tryton-open',
accel_path='<tryton>/Form/Report'),
ToolbarItem(
- id='print_email',
- label=_("_E-Mail..."),
- icon_name='tryton-email',
- accel_path='<tryton>/Form/Email'),
- ToolbarItem(
id='print',
label=_("_Print..."),
icon_name='tryton-print',
accel_path='<tryton>/Form/Print'),
+ ToolbarItem(
+ id='email',
+ label=_("_E-Mail..."),
+ tooltip=_("Send an e-mail using the record"),
+ icon_name='tryton-email',
+ accel_path='<tryton>/Form/Email'),
None,
ToolbarItem(
id='export',
diff -r 5ce1d6984748 -r 83287ed2d759
tryton/gui/window/view_form/view/form_gtk/richtextbox.py
--- a/tryton/gui/window/view_form/view/form_gtk/richtextbox.py Sat Sep 05
22:59:38 2020 +0200
+++ b/tryton/gui/window/view_form/view/form_gtk/richtextbox.py Thu Sep 17
17:18:02 2020 +0200
@@ -1,19 +1,15 @@
# This file is part of Tryton. The COPYRIGHT file at the top level of
# this repository contains the full copyright notices and license terms.
-from gettext import gettext as _
from weakref import WeakKeyDictionary
-from gi.repository import Gtk, Gdk
+from gi.repository import Gtk
from .textbox import TextBox
-from tryton.common import get_toplevel_window, IconFactory
-from tryton.common.htmltextbuffer import (serialize, deserialize,
- setup_tags, normalize_markup, remove_tags, register_foreground,
- FAMILIES, SIZE2SCALE, MIME, use_serialize_func)
+from tryton.common.htmltextbuffer import normalize_markup
+from tryton.common.richtext import (
+ register_format, set_content, get_content, add_toolbar)
from tryton.config import CONFIG
-SIZES = sorted(SIZE2SCALE.keys())
-
class RichTextBox(TextBox):
@@ -24,99 +20,19 @@
self.tags = {}
self.colors = {}
if int(self.attrs.get('toolbar', 1)):
- self.toolbar = self.get_toolbar(self.textview)
+ self.toolbar = add_toolbar(self.textview)
+ self.toolbar.set_style({
+ 'default': False,
+ 'both': Gtk.ToolbarStyle.BOTH,
+ 'text': Gtk.ToolbarStyle.TEXT,
+ 'icons': Gtk.ToolbarStyle.ICONS,
+ }[CONFIG['client.toolbar']])
self.widget.pack_start(
self.toolbar, expand=False, fill=True, padding=0)
- def get_toolbar(self, textview):
- toolbar = Gtk.Toolbar()
- toolbar.set_style({
- 'default': False,
- 'both': Gtk.ToolbarStyle.BOTH,
- 'text': Gtk.ToolbarStyle.TEXT,
- 'icons': Gtk.ToolbarStyle.ICONS,
- }[CONFIG['client.toolbar']])
- tag_widgets = self.tag_widgets[textview] = {}
-
- for icon, label in [
- ('bold', _("Bold")),
- ('italic', _("Italic")),
- ('underline', _("Underline")),
- ]:
- button = Gtk.ToggleToolButton()
- button.set_icon_widget(IconFactory.get_image(
- 'tryton-format-%s' % icon,
- Gtk.IconSize.SMALL_TOOLBAR))
- button.set_label(label)
- button.connect('toggled', self.toggle_props, icon, textview)
- toolbar.insert(button, -1)
- tag_widgets[icon] = button
-
- toolbar.insert(Gtk.SeparatorToolItem(), -1)
-
- for name, options, active in [
- ('family', FAMILIES, FAMILIES.index('normal')),
- ('size', SIZES, SIZES.index('4')),
- ]:
- combobox = Gtk.ComboBoxText()
- for option in options:
- combobox.append_text(option)
- combobox.set_active(active)
- combobox.set_focus_on_click(False)
- combobox.connect('changed', self.change_props, name, textview)
- tool = Gtk.ToolItem()
- tool.add(combobox)
- toolbar.insert(tool, -1)
- tag_widgets[name] = combobox
-
- toolbar.insert(Gtk.SeparatorToolItem(), -1)
-
- button = None
- for name, label in [
- ('left', _("Align Left")),
- ('center', _("Align Center")),
- ('right', _("Align Right")),
- ('justify', _("Justify")),
- ]:
- icon = 'tryton-format-align-%s' % name
- button = Gtk.RadioToolButton.new_from_widget(button)
- button.set_icon_widget(IconFactory.get_image(
- icon, Gtk.IconSize.SMALL_TOOLBAR))
- button.set_active(icon == 'left')
- button.set_label(label)
- button.connect(
- 'toggled', self.toggle_justification, name, textview)
- toolbar.insert(button, -1)
- tag_widgets[name] = button
-
- toolbar.insert(Gtk.SeparatorToolItem(), -1)
-
- for icon, label in [
- ('foreground', _("Foreground Color")),
- # TODO ('background', _('Background')),
- ]:
- button = Gtk.ToolButton()
- if icon == 'foreground':
- button.set_icon_widget(IconFactory.get_image(
- 'tryton-format-color-text',
- Gtk.IconSize.SMALL_TOOLBAR))
- button.set_label(label)
- button.connect('clicked', self.toggle_color, icon, textview)
- toolbar.insert(button, -1)
- tag_widgets[icon] = button
-
- return toolbar
-
def _get_textview(self):
textview = super(RichTextBox, self)._get_textview()
- text_buffer = textview.get_buffer()
- setup_tags(text_buffer)
- text_buffer.register_serialize_format(str(MIME), serialize, None)
- text_buffer.register_deserialize_format(str(MIME), deserialize, None)
- text_buffer.connect_after(
- 'insert-text', self.insert_text_style, textview)
- textview.connect_after('move-cursor', self.detect_style)
- textview.connect('button-release-event', self.detect_style)
+ register_format(textview)
return textview
def translate_widget(self):
@@ -124,7 +40,7 @@
textview = widget.get_children()[-1].get_child()
if self.toolbar:
widget.pack_start(
- self.get_toolbar(textview), expand=False, fill=True, padding=0)
+ add_toolbar(textview), expand=False, fill=True, padding=0)
return widget
def translate_widget_set_readonly(self, widget, value):
@@ -151,212 +67,12 @@
return False
def set_buffer(self, value, textview):
- text_buffer = textview.get_buffer()
- text_buffer.handler_block_by_func(self.insert_text_style)
- start = text_buffer.get_start_iter()
- end = text_buffer.get_end_iter()
- text_buffer.delete(start, end)
- if use_serialize_func:
- text_buffer.deserialize(text_buffer, MIME, start, value)
- else:
- deserialize(
- text_buffer, text_buffer, start, value,
- text_buffer.deserialize_get_can_create_tags(MIME), None)
- text_buffer.handler_unblock_by_func(self.insert_text_style)
+ set_content(textview, value)
def get_buffer(self, textview):
- text_buffer = textview.get_buffer()
- start = text_buffer.get_start_iter()
- end = text_buffer.get_end_iter()
- if use_serialize_func:
- return text_buffer.serialize(text_buffer, MIME, start, end)
- else:
- return serialize(text_buffer, text_buffer, start, end, None)
+ return get_content(textview)
def _readonly_set(self, value):
super(RichTextBox, self)._readonly_set(value)
if self.toolbar:
self.toolbar.set_sensitive(not value)
-
- def detect_style(self, textview, *args):
- if not self.toolbar:
- return
- tag_widgets = self.tag_widgets[textview]
- text_buffer = textview.get_buffer()
- try:
- start, end = text_buffer.get_selection_bounds()
- except ValueError:
- start = end = text_buffer.get_iter_at_mark(
- text_buffer.get_insert())
-
- def toggle_button(name, values):
- try:
- value, = values
- except ValueError:
- value = False
- button = tag_widgets[name]
- button.handler_block_by_func(self.toggle_props)
- button.set_active(value)
- button.handler_unblock_by_func(self.toggle_props)
-
- def set_combobox(name, indexes):
- try:
- index, = indexes
- except ValueError:
- index = -1
- combobox = tag_widgets[name]
- combobox.handler_block_by_func(self.change_props)
- combobox.set_active(index)
- combobox.handler_unblock_by_func(self.change_props)
-
- def toggle_justification(names, value):
- if len(names) != 1:
- value = False
- for name in names:
- button = tag_widgets[name]
- button.handler_block_by_func(self.toggle_justification)
- button.set_active(value)
- button.handler_unblock_by_func(self.toggle_justification)
-
- bolds, italics, underlines = set(), set(), set()
- families, sizes, justifications = set(), set(), set()
- self.colors['foreground'] = 'black'
-
- iter_ = start.copy()
- while True:
- bold, italic, underline = False, False, False
- family = FAMILIES.index('normal')
- size = SIZES.index('4')
- justification = 'left'
-
- for tag in iter_.get_tags():
- if not tag.props.name:
- continue
- elif tag.props.name == 'bold':
- bold = True
- elif tag.props.name == 'italic':
- italic = True
- elif tag.props.name == 'underline':
- underline = True
- elif tag.props.name.startswith('family'):
- _, family = tag.props.name.split()
- family = FAMILIES.index(family)
- elif tag.props.name.startswith('size'):
- _, size = tag.props.name.split()
- size = SIZES.index(size)
- elif tag.props.name.startswith('justification'):
- _, justification = tag.props.name.split()
- elif tag.props.name.startswith('foreground'):
- _, self.colors['foreground'] = tag.props.name.split()
- bolds.add(bold)
- italics.add(italic)
- underlines.add(underline)
- families.add(family)
- sizes.add(size)
- justifications.add(justification)
-
- iter_.forward_char()
- if iter_.compare(end) > 0:
- iter_ = end
- if iter_.compare(end) == 0:
- break
-
- for name, values in [
- ('bold', bolds),
- ('italic', italics),
- ('underline', underlines)]:
- toggle_button(name, values)
- set_combobox('family', families)
- set_combobox('size', sizes)
- toggle_justification(justifications, True)
-
- def insert_text_style(self, text_buffer, iter_, text, length, textview):
- if not self.toolbar:
- return
- # Text is already inserted so iter_ point to the end
- start = iter_.copy()
- start.backward_chars(length)
- end = iter_.copy()
- # Apply tags activated from toolbar
- for name, widget in self.tag_widgets[textview].items():
- self._apply_tool(text_buffer, name, widget, start, end)
-
- def _apply_tool(self, text_buffer, name, tool, start, end):
- # First test RadioToolButton as they inherit from ToggleToolButton
- if isinstance(tool, Gtk.RadioToolButton):
- name = 'justification %s' % name
- if not tool.get_active():
- remove_tags(text_buffer, start, end, name)
- else:
- remove_tags(text_buffer, start, end, 'justification')
- text_buffer.apply_tag_by_name(name, start, end)
- elif isinstance(tool, Gtk.ToggleToolButton):
- if tool.get_active():
- text_buffer.apply_tag_by_name(name, start, end)
- else:
- text_buffer.remove_tag_by_name(name, start, end)
- elif isinstance(tool, Gtk.ComboBoxText):
- value = tool.get_active_text()
- remove_tags(text_buffer, start, end, name)
- name = '%s %s' % (name, value)
- text_buffer.apply_tag_by_name(name, start, end)
-
- def toggle_props(self, toggle, name, textview):
- text_buffer = textview.get_buffer()
- try:
- start, end = text_buffer.get_selection_bounds()
- except ValueError:
- return
- self._apply_tool(text_buffer, name, toggle, start, end)
-
- def change_props(self, combobox, name, textview):
- text_buffer = textview.get_buffer()
- try:
- start, end = text_buffer.get_selection_bounds()
- except ValueError:
- return
- self._apply_tool(text_buffer, name, combobox, start, end)
-
- def toggle_justification(self, button, name, textview):
- text_buffer = textview.get_buffer()
- try:
- start, end = text_buffer.get_selection_bounds()
- except ValueError:
- insert = text_buffer.get_insert()
- start = text_buffer.get_iter_at_mark(insert)
- end = start.copy()
- start.set_line_offset(0)
- if not end.ends_line():
- end.forward_to_line_end()
- self._apply_tool(text_buffer, name, button, start, end)
-
- def toggle_color(self, button, name, textview):
- text_buffer = textview.get_buffer()
- insert = text_buffer.get_insert()
- try:
- start, end = text_buffer.get_selection_bounds()
- except ValueError:
- start = end = None
- else:
- # Use offset position to preserve across buffer modification
- start = start.get_offset()
- end = end.get_offset()
-
- dialog = Gtk.ColorChooserDialog(
- title=_('Select a color'),
- transient_for=get_toplevel_window(),
- use_alpha=False)
- color = Gdk.RGBA()
- if name in self.colors:
- color.parse(self.colors[name])
- dialog.set_rgba(color)
- if dialog.run() == Gtk.ResponseType.OK:
- color = dialog.get_rgba()
- if start is not None and end is not None:
- start = text_buffer.get_iter_at_offset(start)
- end = text_buffer.get_iter_at_offset(end)
- tag = register_foreground(text_buffer, color)
- remove_tags(text_buffer, start, end, name)
- text_buffer.apply_tag(tag, start, end)
- dialog.destroy()
- text_buffer.place_cursor(text_buffer.get_iter_at_mark(insert))