Mercurial > libervia-web
comparison browser_side/richtext.py @ 254:28d3315a8003
browser_side: isolate the basic stuff of RecipientManager in a new class ListManager:
- renamed most occurences of "recipient" to "contact" and "recipient type" to "contact key" or "list"
- data to represent the lists and autocomplete values are parametrized
- UI elements styles are set by default but can be ovewritten by a sub-class
- popup menu for the list Button element has to be set with registerPopupMenuPanel
- richtext UI uses the definitions from sat.tool.frontends.composition
Know issues:
- drag and drop AutoCompleteTextBox corrupts the list of remaining autocomplete values
- selecting an autocomplete value with the mouse and not keybord is not working properly
author | souliane <souliane@mailoo.org> |
---|---|
date | Sat, 09 Nov 2013 09:38:17 +0100 |
parents | d4e73d9140af |
children | d3c734669577 |
comparison
equal
deleted
inserted
replaced
253:19153af4f327 | 254:28d3315a8003 |
---|---|
25 from dialog import InfoDialog | 25 from dialog import InfoDialog |
26 from pyjamas.ui.DialogBox import DialogBox | 26 from pyjamas.ui.DialogBox import DialogBox |
27 from pyjamas.ui.Label import Label | 27 from pyjamas.ui.Label import Label |
28 from pyjamas.ui.FlexTable import FlexTable | 28 from pyjamas.ui.FlexTable import FlexTable |
29 from pyjamas.ui.HorizontalPanel import HorizontalPanel | 29 from pyjamas.ui.HorizontalPanel import HorizontalPanel |
30 from recipients import RECIPIENT_TYPES, RecipientManager | 30 from list_manager import ListManager |
31 | 31 from sat.tools.frontends import composition |
32 BUTTONS = { | |
33 "bold": {"tip": "Bold", "icon": "media/icons/dokuwiki/toolbar/16/bold.png"}, | |
34 "italic": {"tip": "Italic", "icon": "media/icons/dokuwiki/toolbar/16/italic.png"}, | |
35 "underline": {"tip": "Underline", "icon": "media/icons/dokuwiki/toolbar/16/underline.png"}, | |
36 "code": {"tip": "Code", "icon": "media/icons/dokuwiki/toolbar/16/mono.png"}, | |
37 "strikethrough": {"tip": "Strikethrough", "icon": "media/icons/dokuwiki/toolbar/16/strike.png"}, | |
38 "heading": {"tip": "Heading", "icon": "media/icons/dokuwiki/toolbar/16/hequal.png"}, | |
39 "numberedlist": {"tip": "Numbered List", "icon": "media/icons/dokuwiki/toolbar/16/ol.png"}, | |
40 "list": {"tip": "List", "icon": "media/icons/dokuwiki/toolbar/16/ul.png"}, | |
41 "link": {"tip": "Link", "icon": "media/icons/dokuwiki/toolbar/16/linkextern.png"}, | |
42 "horizontalrule": {"tip": "Horizontal rule", "icon": "media/icons/dokuwiki/toolbar/16/hr.png"} | |
43 } | |
44 | |
45 # Define here your formats, the key must match the ones used in button. | |
46 # Tupples values must have 3 elements : prefix to the selection or cursor | |
47 # position, sample text to write if the marker is not applied on a selection, | |
48 # suffix to the selection or cursor position. | |
49 # FIXME: must be moved in backend and not harcoded like this | |
50 FORMATS = {"markdown": {"bold": ("**", "bold", "**"), | |
51 "italic": ("*", "italic", "*"), | |
52 "code": ("`", "code", "`"), | |
53 "heading": ("\n# ", "Heading 1", "\n## Heading 2\n"), | |
54 "list": ("\n* ", "item", "\n + subitem\n"), | |
55 "link": ("[desc](", "link", ")"), | |
56 "horizontalrule": ("\n***\n", "", "") | |
57 }, | |
58 "bbcode": {"bold": ("[b]", "bold", "[/b]"), | |
59 "italic": ("[i]", "italic", "[/i]"), | |
60 "underline": ("[u]", "underline", "[/u]"), | |
61 "strikethrough": ("[s]", "strikethrough", "[/s]"), | |
62 "code": ("[code]", "code", "[/code]"), | |
63 "link": ("[url=", "link", "]desc[/url]"), | |
64 "list": ("\n[list] [*]", "item 1", " [*]item 2 [/list]\n") | |
65 }, | |
66 "dokuwiki": {"bold": ("**", "bold", "**"), | |
67 "italic": ("//", "italic", "//"), | |
68 "underline": ("__", "underline", "__"), | |
69 "strikethrough": ("<del>", "strikethrough", "</del>"), | |
70 "code": ("<code>", "code", "</code>"), | |
71 "heading": ("\n==== ", "Heading 1", " ====\n=== Heading 2 ===\n"), | |
72 "link": ("[[", "link", "|desc]]"), | |
73 "list": ("\n * ", "item\n", "\n * subitem\n"), | |
74 "horizontalrule": ("\n----\n", "", "") | |
75 }, | |
76 "XHTML": {"bold": ("<b>", "bold", "</b>"), | |
77 "italic": ("<i>", "italic", "</i>"), | |
78 "underline": ("<u>", "underline", "</u>"), | |
79 "strikethrough": ("<s>", "strikethrough", "</s>"), | |
80 "code": ("<pre>", "code", "</pre>"), | |
81 "heading": ("\n<h3>", "Heading 1", "</h3>\n<h4>Heading 2</h4>\n"), | |
82 "link": ("<a href=\"", "link", "\">desc</a>"), | |
83 "list": ("\n<ul><li>", "item 1", "</li><li>item 2</li></ul>\n"), | |
84 "horizontalrule": ("\n<hr/>\n", "", "") | |
85 } | |
86 | |
87 } | |
88 | |
89 PARAM_KEY = "Composition" | |
90 PARAM_NAME = "Syntax" | |
91 | 32 |
92 | 33 |
93 class RichTextEditor(FlexTable): | 34 class RichTextEditor(FlexTable): |
94 """Panel for the rich text editor.""" | 35 """Panel for the rich text editor.""" |
95 | 36 |
96 def __init__(self, host, parent=None, onCloseCallback=None): | 37 def __init__(self, host, parent, onCloseCallback=None): |
97 """Fill the editor with recipients panel, toolbar, text area...""" | 38 """Fill the editor with recipients panel, toolbar, text area...""" |
98 | 39 |
99 # TODO: don't forget to comment this before commit | 40 # TODO: don't forget to comment this before commit |
100 self._debug = False | 41 self._debug = False |
101 | 42 |
102 # This must be done before FlexTable.__init__ because it is used by setVisible | 43 # This must be done before FlexTable.__init__ because it is used by setVisible |
103 self.host = host | 44 self.host = host |
104 | 45 |
105 offset1 = len(RECIPIENT_TYPES) | 46 offset1 = len(composition.RECIPIENT_TYPES) |
106 offset2 = len(FORMATS) if self._debug else 1 | 47 offset2 = len(composition.RICH_FORMATS) if self._debug else 1 |
107 FlexTable.__init__(self, offset1 + offset2 + 2, 2) | 48 FlexTable.__init__(self, offset1 + offset2 + 2, 2) |
108 self.addStyleName('richTextEditor') | 49 self.addStyleName('richTextEditor') |
109 | 50 |
110 self._parent = parent | 51 self._parent = parent |
111 self._on_close_callback = onCloseCallback | 52 self._on_close_callback = onCloseCallback |
112 | 53 |
113 # recipient types sub-panels are automatically added by the manager | 54 # recipient types sub-panels are automatically added by the manager |
114 self.recipient = RecipientManager(self) | 55 self.recipient = RecipientManager(self) |
115 self.recipient.createWidgets() | 56 self.recipient.createWidgets(title_format="%s: ") |
116 | 57 |
117 # Rich text tool bar is automatically added by setVisible | 58 # Rich text tool bar is automatically added by setVisible |
118 | 59 |
119 self.textarea = TextArea() | 60 self.textarea = TextArea() |
120 self.textarea.addStyleName('richTextArea') | 61 self.textarea.addStyleName('richTextArea') |
136 Add it to parent if parent is not None, otherwise display it | 77 Add it to parent if parent is not None, otherwise display it |
137 in a popup dialog. Information are saved for later the widget | 78 in a popup dialog. Information are saved for later the widget |
138 to be also automatically removed from its parent, or the | 79 to be also automatically removed from its parent, or the |
139 popup to be closed. | 80 popup to be closed. |
140 @param host: the host | 81 @param host: the host |
141 @popup parent: parent panel (in a popup if parent == None) . | 82 @param parent: parent panel (or None to display in a popup). |
142 @return: the RichTextEditor instance if popup is False, otherwise | 83 @return: the RichTextEditor instance if parent is not None, |
143 a popup DialogBox containing the RichTextEditor. | 84 otherwise a popup DialogBox containing the RichTextEditor. |
144 """ | 85 """ |
145 if not hasattr(host, 'richtext'): | 86 if not hasattr(host, 'richtext'): |
146 host.richtext = RichTextEditor(host, parent, onCloseCallback) | 87 host.richtext = RichTextEditor(host, parent, onCloseCallback) |
147 | 88 |
148 def add(widget, parent): | 89 def add(widget, parent): |
166 host.richtext.syncFromUniBox() | 107 host.richtext.syncFromUniBox() |
167 return host.richtext.popup if parent is None else host.richtext | 108 return host.richtext.popup if parent is None else host.richtext |
168 | 109 |
169 def setVisible(self, kwargs): | 110 def setVisible(self, kwargs): |
170 """Called each time the widget is displayed, after creation or after having been hidden.""" | 111 """Called each time the widget is displayed, after creation or after having been hidden.""" |
171 self.host.bridge.call('asyncGetParamA', self.setToolBar, PARAM_NAME, PARAM_KEY) or self.setToolBar(None) | 112 self.host.bridge.call('asyncGetParamA', self.setToolBar, composition.PARAM_NAME_SYNTAX, composition.PARAM_KEY_COMPOSITION) or self.setToolBar(None) |
172 FlexTable.setVisible(self, kwargs) | 113 FlexTable.setVisible(self, kwargs) |
173 | 114 |
174 def __close(self): | 115 def __close(self): |
175 """Remove the widget from parent or close the popup.""" | 116 """Remove the widget from parent or close the popup.""" |
176 if self._parent is None: | 117 if self._parent is None: |
183 def setToolBar(self, _format): | 124 def setToolBar(self, _format): |
184 """This method is called asynchronously after the parameter | 125 """This method is called asynchronously after the parameter |
185 holding the rich text format is retrieved. It is called at | 126 holding the rich text format is retrieved. It is called at |
186 each opening of the rich text editor because the user may | 127 each opening of the rich text editor because the user may |
187 have change his setting since the last time.""" | 128 have change his setting since the last time.""" |
188 if _format is None or _format not in FORMATS.keys(): | 129 if _format is None or _format not in composition.RICH_FORMATS.keys(): |
189 _format = FORMATS.keys()[0] | 130 _format = composition.RICH_FORMATS.keys()[0] |
190 if hasattr(self, "_format") and self._format == _format: | 131 if hasattr(self, "_format") and self._format == _format: |
191 return | 132 return |
192 self._format = _format | 133 self._format = _format |
193 offset1 = len(RECIPIENT_TYPES) | 134 offset1 = len(composition.RECIPIENT_TYPES) |
194 count = 0 | 135 count = 0 |
195 for _format in FORMATS.keys() if self._debug else [self._format]: | 136 for _format in composition.RICH_FORMATS.keys() if self._debug else [self._format]: |
196 toolbar = HorizontalPanel() | 137 toolbar = HorizontalPanel() |
197 toolbar.addStyleName('richTextToolbar') | 138 toolbar.addStyleName('richTextToolbar') |
198 for key in FORMATS[_format].keys(): | 139 for key in composition.RICH_FORMATS[_format].keys(): |
199 self.addToolbarButton(toolbar, _format, key) | 140 self.addToolbarButton(toolbar, _format, key) |
200 label = Label("Format: %s" % _format) | 141 label = Label("Format: %s" % _format) |
201 label.addStyleName("richTextFormatLabel") | 142 label.addStyleName("richTextFormatLabel") |
202 toolbar.add(label) | 143 toolbar.add(label) |
203 self.getFlexCellFormatter().setColSpan(offset1 + count, 0, 2) | 144 self.getFlexCellFormatter().setColSpan(offset1 + count, 0, 2) |
205 count += 1 | 146 count += 1 |
206 | 147 |
207 def addToolbarButton(self, toolbar, _format, key): | 148 def addToolbarButton(self, toolbar, _format, key): |
208 """Add a button with the defined parameters.""" | 149 """Add a button with the defined parameters.""" |
209 button = Button('<img src="%s" class="richTextIcon" />' % | 150 button = Button('<img src="%s" class="richTextIcon" />' % |
210 BUTTONS[key]["icon"]) | 151 composition.RICH_BUTTONS[key]["icon"]) |
211 button.setTitle(BUTTONS[key]["tip"]) | 152 button.setTitle(composition.RICH_BUTTONS[key]["tip"]) |
212 button.addStyleName('richTextToolButton') | 153 button.addStyleName('richTextToolButton') |
213 toolbar.add(button) | 154 toolbar.add(button) |
214 | 155 |
215 def button_callback(): | 156 def button_callback(): |
216 """Generic callback for a toolbar button.""" | 157 """Generic callback for a toolbar button.""" |
217 text = self.textarea.getText() | 158 text = self.textarea.getText() |
218 cursor_pos = self.textarea.getCursorPos() | 159 cursor_pos = self.textarea.getCursorPos() |
219 selection_length = self.textarea.getSelectionLength() | 160 selection_length = self.textarea.getSelectionLength() |
220 infos = FORMATS[_format][key] | 161 infos = composition.RICH_FORMATS[_format][key] |
221 if selection_length == 0: | 162 if selection_length == 0: |
222 middle_text = infos[1] | 163 middle_text = infos[1] |
223 else: | 164 else: |
224 middle_text = text[cursor_pos:cursor_pos + selection_length] | 165 middle_text = text[cursor_pos:cursor_pos + selection_length] |
225 self.textarea.setText(text[:cursor_pos] | 166 self.textarea.setText(text[:cursor_pos] |
233 button.addClickListener(button_callback) | 174 button.addClickListener(button_callback) |
234 | 175 |
235 def syncFromUniBox(self): | 176 def syncFromUniBox(self): |
236 """Synchronize from unibox.""" | 177 """Synchronize from unibox.""" |
237 data, target = self.host.uni_box.getTargetAndData() | 178 data, target = self.host.uni_box.getTargetAndData() |
238 self.recipient.setRecipients({"To": [target]} if target else {}) | 179 self.recipient.setContacts({"To": [target]} if target else {}) |
239 self.textarea.setText(data if data else "") | 180 self.textarea.setText(data if data else "") |
240 | 181 |
241 def syncToUniBox(self, recipients=None): | 182 def syncToUniBox(self, recipients=None): |
242 """Synchronize to unibox if a maximum of one recipient is set, | 183 """Synchronize to unibox if a maximum of one recipient is set, |
243 and it is not set to for optional recipient type. That means | 184 and it is not set to for optional recipient type. That means |
244 synchronization is not done if more then one recipients are set | 185 synchronization is not done if more then one recipients are set |
245 or if a recipient is set to an optional type (Cc, Bcc). | 186 or if a recipient is set to an optional type (Cc, Bcc). |
246 @return True if the sync could be done, False otherwise""" | 187 @return True if the sync could be done, False otherwise""" |
247 if recipients is None: | 188 if recipients is None: |
248 recipients = self.recipient.getRecipients() | 189 recipients = self.recipient.getContacts() |
249 target = "" | 190 target = "" |
250 # we could eventually allow more in the future | 191 # we could eventually allow more in the future |
251 allowed = 1 | 192 allowed = 1 |
252 for key in recipients: | 193 for key in recipients: |
253 count = len(recipients[key]) | 194 count = len(recipients[key]) |
254 if count == 0: | 195 if count == 0: |
255 continue | 196 continue |
256 allowed -= count | 197 allowed -= count |
257 if allowed < 0 or RECIPIENT_TYPES[key]["optional"]: | 198 if allowed < 0 or composition.RECIPIENT_TYPES[key]["optional"]: |
258 return False | 199 return False |
259 # TODO: change this if later more then one recipients are allowed | 200 # TODO: change this if later more then one recipients are allowed |
260 target = recipients[key][0] | 201 target = recipients[key][0] |
261 self.host.uni_box.setText(self.textarea.getText()) | 202 self.host.uni_box.setText(self.textarea.getText()) |
262 from panels import ChatPanel, MicroblogPanel | 203 from panels import ChatPanel, MicroblogPanel |
292 " in the rich text editor, and send your message directly" + | 233 " in the rich text editor, and send your message directly" + |
293 " from here.", Width="400px").center() | 234 " from here.", Width="400px").center() |
294 | 235 |
295 def sendMessage(self): | 236 def sendMessage(self): |
296 """Send the message.""" | 237 """Send the message.""" |
297 recipients = self.recipient.getRecipients() | 238 recipients = self.recipient.getContacts() |
298 if self.syncToUniBox(recipients): | 239 if self.syncToUniBox(recipients): |
299 # also check that we actually have a message target and data | 240 # also check that we actually have a message target and data |
300 if len(recipients["To"]) > 0 and self.textarea.getText() != "": | 241 if len(recipients["To"]) > 0 and self.textarea.getText() != "": |
301 from pyjamas.ui.KeyboardListener import KEY_ENTER | 242 from pyjamas.ui.KeyboardListener import KEY_ENTER |
302 self.host.uni_box.onKeyPress(self.host.uni_box, KEY_ENTER, None) | 243 self.host.uni_box.onKeyPress(self.host.uni_box, KEY_ENTER, None) |
307 " but it has been stored to the quick box instead.", Width="400px").center() | 248 " but it has been stored to the quick box instead.", Width="400px").center() |
308 return | 249 return |
309 InfoDialog("Feature in development", | 250 InfoDialog("Feature in development", |
310 "Sending a message to more the one recipient," + | 251 "Sending a message to more the one recipient," + |
311 " to Cc or Bcc is not implemented yet!", Width="400px").center() | 252 " to Cc or Bcc is not implemented yet!", Width="400px").center() |
253 | |
254 | |
255 class RecipientManager(ListManager): | |
256 """A manager for sub-panels to set the recipients for each recipient type.""" | |
257 | |
258 def __init__(self, parent): | |
259 # TODO: be sure we also display empty groups and disconnected contacts + their groups | |
260 # store the full list of potential recipients (groups and contacts) | |
261 list_ = [] | |
262 list_.extend("@%s" % group for group in parent.host.contact_panel.getGroups()) | |
263 list_.extend(contact for contact in parent.host.contact_panel.getContacts()) | |
264 ListManager.__init__(self, parent, composition.RECIPIENT_TYPES, list_) | |
265 | |
266 self.registerPopupMenuPanel(entries=composition.RECIPIENT_TYPES, | |
267 hide=lambda sender, key: self.__children[key]["panel"].isVisible(), | |
268 callback=self.setContactPanelVisible) |