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)