Package qubx :: Module treeGTK
[hide private]
[frames] | no frames]

Source Code for Module qubx.treeGTK

   1  """pygtk support for qubtree: clipboard and visual editing. 
   2   
   3  Copyright 2007-2012 Research Foundation State University of New York  
   4  This file is part of QUB Express.                                           
   5   
   6  QUB Express is free software; you can redistribute it and/or modify           
   7  it under the terms of the GNU General Public License as published by  
   8  the Free Software Foundation, either version 3 of the License, or     
   9  (at your option) any later version.                                   
  10   
  11  QUB Express is distributed in the hope that it will be useful,                
  12  but WITHOUT ANY WARRANTY; without even the implied warranty of        
  13  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the         
  14  GNU General Public License for more details.                          
  15   
  16  You should have received a copy of the GNU General Public License,    
  17  named LICENSE.txt, in the QUB Express program directory.  If not, see         
  18  <http://www.gnu.org/licenses/>.                                       
  19   
  20  """ 
  21   
  22  import qubx.tree 
  23  from qubx.util_types import * 
  24  from qubx.GTK import * 
  25  import qubx.pyenv 
  26  import qubx.pyenvGTK 
  27  import sys 
  28  import os 
  29  import re 
  30  from pprint import pprint 
  31   
  32  ######### clipboard support ############### 
  33   
34 -def Copy(node, clipboard=None):
35 """Copies node to the specified (def main) clipboard.""" 36 targets = [ ("STRING", 0, 0), 37 ("TEXT", 0, 1), 38 ("COMPOUND_TEXT", 0, 2), 39 ("UTF8_STRING", 0, 3), 40 ("qubtree", 0, 4) ] 41 bin = [ node.getBytes() ] 42 def get(clipboard, selectiondata, info, data): 43 if info == 4: 44 selectiondata.set("qubtree", 8, bin[0]) 45 else: 46 selectiondata.set_text(str(qubx.tree.ReadBytes(bin[0])))
47 def clear(clipboard, data): 48 bin[0] = '' 49 50 clip = clipboard or gtk.clipboard_get("CLIPBOARD") 51 clip.set_with_data(targets, get, clear, None) 52
53 -def CanPaste(clipboard=None):
54 """Returns True if there is a qubx.tree.Node in the specified (def main) clipboard.""" 55 clip = clipboard or gtk.clipboard_get("CLIPBOARD") 56 targets = clip.wait_for_targets() 57 return bool(targets and ("qubtree" in targets))
58
59 -def Paste(clipboard=None):
60 """ 61 Returns a copy of the qubx.tree.Node from the specified (def main) clipboard. 62 If there is none, or any exception occurs, node.isNull == True.""" 63 try: 64 clip = clipboard or gtk.clipboard_get("CLIPBOARD") 65 seldata = clip.wait_for_contents("qubtree") 66 return qubx.tree.ReadBytes(seldata.data) 67 except: 68 traceback.print_exc() 69 return qubx.tree.NullNode()
70 71 ######### node.data <--> string (one-line entry) ############### 72
73 -def acceptNumList(s):
74 l = eval(s, qubx.pyenv.env.globals) 75 try: 76 l = [0+l] # list-ify a number 77 except: 78 try: 79 sum(l) # test they're all numbers 80 except: 81 sum(sum(l, [])) # or lists of numbers 82 return l
83
84 -def acceptNodeData(node):
85 """ 86 acceptNodeData(node) -> accept; 87 accept(string) -> action_fn; 88 action_fn() -> modifies node.data; 89 e.g. 90 91 >>> try: 92 >>> acceptNodeData(node)(string)() 93 >>> except: 94 >>> invalid string 95 """ 96 def assign(x): 97 def doit(): 98 node.data = x
99 doit.val = x 100 return doit 101 def slice_ass(l): 102 def doit(): 103 typ = node.data and node.data.type or qubx.tree.QTR_TYPE_DOUBLE 104 try: 105 sum(l) # if it's 1-D: 106 if (node.data.rows > 1) and (node.data.cols > 1): 107 node.data.setup(typ, len(l), 1) 108 elif (not node.data) and (len(l) >= 1): 109 node.data = l[0] 110 node.data.resize(len(l)) 111 else: 112 node.data.resize(len(l)) 113 node.data[0:len(l)] = l 114 except: # 2-D: 115 nc = l and len(l[0]) or 0 116 nr = len(l) 117 node.data.setup(typ, nr, nc) 118 for i,r in enumerate(l): 119 node.data[i*nc:(i+1)*nc] = r[0:nc] 120 doit.val = l 121 return doit 122 def accept(s): 123 if not s: 124 return assign(None) # blank: clear 125 elif node.data and (node.data.loadedRows != (0, node.data.rows-1)): 126 raise Exception("can't handle unloaded data") 127 elif node.data.type == qubx.tree.QTR_TYPE_STRING: 128 return assign(s) 129 else: 130 try: 131 return slice_ass( acceptNumList(s) ) 132 except: 133 if node.data: 134 raise Exception('expected number(s)') 135 return assign(s) 136 return accept 137
138 -def captionNodeData(node):
139 if node.data.type == qubx.tree.QTR_TYPE_STRING: 140 return 'Edit data for "%s" as a string:' % node.name 141 elif node.data.type in [qubx.tree.QTR_TYPE_UNKNOWN, qubx.tree.QTR_TYPE_POINTER]: 142 return 'Unknown data for "%s" -- blank to remove it.' % node.name 143 elif not node.data: 144 return 'Enter data for "%s" as a number, list of numbers, or a string (no quotes).' % node.name 145 elif node.data.loadedRows != (0, node.data.rows-1): 146 return 'Data for "%s" is not fully loaded -- blank to remove it all.' % node.name 147 else: 148 return 'Edit numeric data for "%s"' % node.name
149 150 ex_numeric = """Examples: 151 (numeric) 152 1 153 1.0 154 pi 155 [2, 3] 156 [2.0, 3.0] 157 [[2.0, 3.0], [4.0, 5.0]] 158 [[2.0], [3.0]] 159 [float(x) for x in xrange(100)] 160 """ 161
162 -def helpNodeData(node):
163 if node.data.type == qubx.tree.QTR_TYPE_STRING: 164 return 'To change to numeric data, first clear the data and close this window. Then enter an integer or floating point number.' 165 elif node.data.type in [qubx.tree.QTR_TYPE_UNKNOWN, qubx.tree.QTR_TYPE_POINTER]: 166 return 'details: %s\n%d x %d' % qubx.tree.TYPENAMES[node.data.type], node.data.rows, node.data.cols 167 elif not node.data: 168 return ex_numeric+""" 169 (string) 170 whatever 171 """ 172 elif node.data.loadedRows != (0, node.data.rows-1): 173 "To edit this data, double-click the node in tree-view, or right-click it -> Properties." 174 else: 175 return ex_numeric+""" 176 data type (%s) is preserved. If you'd like to change it, e.g. from float to int or even string, first clear the data and close this window. Then enter a number, with or without decimal point as desired. 177 """ % qubx.tree.TYPENAMES[node.data.type]
178
179 -def formatNodeData(node):
180 d = node.data 181 t = d.type 182 ld = d.loadedRows 183 if t == qubx.tree.QTR_TYPE_STRING: 184 return str(node.data) 185 elif t == qubx.tree.QTR_TYPE_EMPTY: 186 return "" 187 elif t in [qubx.tree.QTR_TYPE_POINTER, qubx.tree.QTR_TYPE_UNKNOWN]: 188 return "..." 189 elif ld == (-1, -1): 190 return "..." 191 elif ld == (0, d.rows-1): 192 if d.cols == 1: 193 if d.rows == 1: 194 return str(d[0]) 195 elif d.rows <= 10: 196 return str([d[i] for i in xrange(d.rows)]) 197 else: 198 return str([d[i] for i in xrange(10)]+['...']) 199 else: 200 r = d.row(0) 201 if d.cols <= 10: 202 return str([ r[:], '...']) 203 else: 204 return str([ r[:10]+'...', '...']) 205 else: # partial load 206 return "..."
207
208 -def nodeDataIsFullyShown(data):
209 return not bool(re.search(r"\.\.\.", data))
210 211 ######### gtk.TreeView-based browser ############### 212
213 -class TreeView(gtk.TreeView):
214 """ 215 gtk.Widget for browsing/editing a qubx.tree.Node and its children. 216 217 @ivar root: a qubx.tree.Node. no-one but this TreeView should edit root while it's assigned here 218 @ivar changed: whether this TreeView has made any changes since root was assigned 219 @ivar OnChange: L{WeakEvent}(TreeView) 220 """ 221 222 __explore_featured = ['col_name', 'cell_name', 'col_data', 'cell_data', 'OnChange', 'root', 'changed', 'add_children'] 223
224 - def __init__(self):
225 gtk.TreeView.__init__(self) 226 self.__ref = Reffer() 227 self._build_ui() 228 229 self.connect('button_press_event', self._onButtonPress) 230 self.connect('row_activated', self._onRowActivated) 231 self.cell_name.connect('edited', self._onEditName) 232 self.cell_data.connect('edited', self._onEditData) 233 234 self.OnChange = WeakEvent() # (TreeView) 235 self.root = qubx.tree.NullNode()
236
237 - def _build_ui(self):
238 self.set_size_request(200, 70) 239 self.popup = gtk.Menu() 240 self.itemCut = build_menuitem('Cut', self.__ref(self._onItemCut), menu=self.popup) 241 self.itemCopy = build_menuitem('Copy', self.__ref(self._onItemCopy), menu=self.popup) 242 self.itemPasteAfter = build_menuitem('Paste After', self.__ref(self._onItemPasteAfter), menu=self.popup) 243 self.itemPasteChild = build_menuitem('Paste Child', self.__ref(self._onItemPasteChild), menu=self.popup) 244 self.itemInsertAfter = build_menuitem('Insert After', self.__ref(self._onItemInsertAfter), menu=self.popup) 245 self.itemInsertChild = build_menuitem('Insert Child', self.__ref(self._onItemInsertChild), menu=self.popup) 246 self.itemDelete = build_menuitem('Delete', self.__ref(self._onItemDelete), menu=self.popup) 247 self.itemProperties = build_menuitem('Properties', self.__ref(self._onItemProperties), menu=self.popup) 248 249 self.store = gtk.TreeStore(str, str, gobject.TYPE_PYOBJECT, bool) 250 self.set_model(self.store) 251 self.col_name = gtk.TreeViewColumn('Name') 252 self.append_column(self.col_name) 253 self.cell_name = gtk.CellRendererText() 254 self.cell_name.set_property('editable', True) 255 self.col_name.pack_start(self.cell_name, True) 256 self.col_name.add_attribute(self.cell_name, 'text', 0) 257 self.col_data = gtk.TreeViewColumn('Data') 258 self.append_column(self.col_data) 259 self.cell_data = gtk.CellRendererText() 260 #self.cell_data.set_property('editable', True) 261 self.col_data.pack_start(self.cell_data, True) 262 self.col_data.add_attribute(self.cell_data, 'text', 1) 263 self.col_data.add_attribute(self.cell_data, 'editable', 3)
264
265 - def set_root(self, root):
266 self._root = root 267 self.changed = False 268 self.store.clear() 269 data = formatNodeData(root) 270 rootI = self.store.append(None, [root.name, data, root, nodeDataIsFullyShown(data)]) 271 count = self.add_children(root, rootI) 272 if count < 20: 273 self.expand_all() 274 else: 275 self.expand_row((0,), False)
276 root = property(lambda s: s._root, set_root) 277
278 - def set_changed(self, x):
279 if x: 280 self.OnChange(self) 281 self._changed = x
282 changed = property(lambda s: s._changed, set_changed) 283
284 - def add_children(self, node, iter):
285 count = 0 286 for ch in qubx.tree.children(node): 287 data = formatNodeData(ch) 288 chI = self.store.append(iter, [ch.name, data, ch, nodeDataIsFullyShown(data)]) 289 count += 1 290 count += self.add_children(ch, chI) 291 return count
292
293 - def _onEditName(self, cell, path_string, new_text):
294 path = tuple(re.split(':',path_string)) 295 if len(path) == 1: # root 296 return 297 node = self.store.get(self.store.get_iter(path_string), 2)[0] 298 node.name = new_text 299 self.store.set(self.store.get_iter(path_string), 0, new_text) 300 self.changed = True
301 - def _onEditData(self, cell, path_string, new_text):
302 node = self.store.get(self.store.get_iter(path_string), 2)[0] 303 accept = acceptNodeData(node) 304 def use(txt): 305 accept(txt)() 306 data = formatNodeData(node) 307 self.store.set(self.store.get_iter(path_string), 1, data) 308 self.store.set(self.store.get_iter(path_string), 3, nodeDataIsFullyShown(data)) 309 self.changed = True
310 try: 311 use(new_text) 312 except: 313 dlg = qubx.pyenvGTK.HelpTestDialog(accept, captionNodeData(node), "data = ", helpNodeData(node), 314 lambda x: qubx.pyenv.env.globals.__setitem__('data', x.val), lambda x:'data') 315 if gtk.RESPONSE_ACCEPT == dlg.run(new_text): 316 try: 317 use(dlg.expr) 318 except: 319 pass 320 dlg.destroy()
321 - def _onButtonPress(self, view, event):
322 if event.button == 3: 323 try: 324 path, col, cellx, celly = view.get_path_at_pos(int(event.x), int(event.y)) 325 except: 326 return False 327 # just in case the view doesn't alreay have focus 328 view.grab_focus() 329 selection = view.get_selection() 330 # if this row isn't already selected, then select it before popup 331 if not selection.path_is_selected(path): 332 view.set_cursor( path, col, False) 333 self.itemPath = path 334 self.itemCopy.set_sensitive(True) 335 self.itemPasteChild.set_sensitive(CanPaste()) 336 self.itemInsertChild.set_sensitive(True) 337 self.itemCut.set_sensitive(len(path)>1) 338 self.itemPasteAfter.set_sensitive(len(path)>1 and CanPaste()) 339 self.itemInsertAfter.set_sensitive(len(path)>1) 340 self.itemDelete.set_sensitive(len(path)>1) 341 self.itemProperties.set_sensitive(True) 342 self.popup.popup(None, None, None, 0, event.time) 343 return True 344 return False
345 - def _onRowActivated(self, tree, path, col):
346 self.itemPath = path 347 self._onItemProperties(self.itemProperties)
348 - def _onItemCut(self, item):
349 hereI = self.store.get_iter(self.itemPath) 350 node = self.store.get(hereI, 2)[0] 351 if node == self.root: 352 return 353 Copy(node) 354 node.parent.remove(node) 355 self.store.remove(hereI) 356 self.changed = True
357 - def _onItemCopy(self, item):
358 hereI = self.store.get_iter(self.itemPath) 359 node = self.store.get(hereI, 2)[0] 360 Copy(node)
361 - def _onItemPasteAfter(self, item):
362 hereI = self.store.get_iter(self.itemPath) 363 node = self.store.get(hereI, 2)[0] 364 if node == self.root: 365 return 366 parentI = self.store.get_iter(self.itemPath[:-1]) 367 try: 368 sib = Paste() 369 if sib.isNull: 370 return 371 node.parent.insert(sib, node) 372 data = formatNodeData(sib) 373 chI = self.store.insert_after(parentI, hereI, [sib.name, data, sib, nodeDataIsFullyShown(data)]) 374 self.add_children(sib, chI) 375 self.set_cursor(self.store.get_path(chI), self.col_name, False) 376 self.changed = True 377 except: 378 traceback.print_exc()
379 - def _onItemPasteChild(self, item):
380 hereI = self.store.get_iter(self.itemPath) 381 node = self.store.get(hereI, 2)[0] 382 try: 383 ch = Paste() 384 if ch.isNull: 385 return 386 node.insert(ch) 387 data = formatNodeData(ch) 388 chI = self.store.insert_after(hereI, None, [ch.name, data, ch, nodeDataIsFullyShown(data)]) 389 self.add_children(ch, chI) 390 self.expand_row(self.itemPath, False) 391 self.set_cursor(self.store.get_path(chI), self.col_name, 0) 392 self.changed = True 393 except: 394 traceback.print_exc()
395 - def _onItemInsertAfter(self, item):
396 hereI = self.store.get_iter(self.itemPath) 397 node = self.store.get(hereI, 2)[0] 398 if node == self.root: 399 return 400 parentI = self.store.get_iter(self.itemPath[:-1]) 401 sib = node.parent.insert('untitled', node) 402 data = formatNodeData(sib) 403 chI = self.store.insert_after(parentI, hereI, [sib.name, data, sib, nodeDataIsFullyShown(data)]) 404 self.set_cursor(self.store.get_path(chI), self.col_name, True) 405 self.changed = True
406 - def _onItemInsertChild(self, item):
407 hereI = self.store.get_iter(self.itemPath) 408 node = self.store.get(hereI, 2)[0] 409 ch = node.insert('untitled') 410 data = formatNodeData(ch) 411 chI = self.store.insert_after(hereI, None, [ch.name, data, ch, nodeDataIsFullyShown(data)]) 412 self.expand_row(self.itemPath, False) 413 self.set_cursor(self.store.get_path(chI), self.col_name, True) 414 self.changed = True
415 - def _onItemDelete(self, item):
416 hereI = self.store.get_iter(self.itemPath) 417 node = self.store.get(hereI, 2)[0] 418 if node == self.root: 419 return 420 node.parent.remove(node) 421 self.store.remove(hereI) 422 self.changed = True
423 - def _onItemProperties(self, item):
424 hereI = self.store.get_iter(self.itemPath) 425 node = self.store.get(hereI, 2)[0] 426 dlg = NodeDialog() 427 dlg.run(node) 428 dlg.destroy() 429 self.store.set(hereI, 0, node.name) 430 data = formatNodeData(node) 431 self.store.set(hereI, 1, data) 432 self.store.set(hereI, 3, nodeDataIsFullyShown(data)) 433 self.changed = True
434 435 436 ######### node properties widget and dialog ############### 437
438 -class NodeView(gtk.VBox):
439 """ 440 Properties panel for a single qubx.tree.Node. 441 442 @ivar node: a L{qubx.tree.Node}; do not edit node's name or data while it is assigned here 443 """ 444 445 __explore_featured = ['node', 'refresh', 'reshape', 'reparse'] 446
447 - def __init__(self):
448 gtk.VBox.__init__(self) 449 self._build_ui() 450 451 self.ref = Reffer() 452 self.txtName.OnChange += self.ref(self._onChangeName) 453 self.mnuType.OnPopulate += self.ref(self._onPopulateType) 454 self.mnuType.OnChanged += self.ref(self._onChangeType) 455 self.chkPreload.connect('toggled', self._onTogglePreload) 456 self.txtRows.OnChange += self.ref(self._onChangeRows) 457 self.txtCols.OnChange += self.ref(self._onChangeCols) 458 self.btnExport.connect('clicked', self._onPressExport) 459 self.btnImport.connect('clicked', self._onPressImport) 460 self.txtLoadFrom.OnChange += self.ref(self._onChangeLoadFrom) 461 self.txtLoadTo.OnChange += self.ref(self._onChangeLoadTo) 462 self.txtData.connect('focus_out_event', self._onFocusOutData) 463 self.btnApply.connect('clicked', self._onPressApply) 464 465 self.node = None
466
467 - def _build_ui(self):
468 self.set_size_request(400, 200) 469 self.header = pack_item(gtk.HBox(True), self) 470 h = pack_item(gtk.HBox(), self.header) 471 pack_label('Name', h) 472 self.txtName = pack_item(NumEntry(""), h, expand=True) 473 h = pack_item(gtk.HBox(), self.header) 474 pack_label('Data type:', h) 475 self.mnuType = pack_item(DynamicComboBox(), h, expand=True) 476 self.header2 = pack_item(gtk.HBox(), self) 477 h = self.header2 478 self.chkPreload = pack_check('Preload', h) 479 self.panRC = pack_item(gtk.HBox(), h, expand=True) 480 self.txtRows = pack_item(NumEntry(1, acceptIntGreaterThan(-1), width_chars=4), self.panRC) 481 pack_label(' x ', self.panRC) 482 self.txtCols = pack_item(NumEntry(1, acceptIntGreaterThan(-1), width_chars=4), self.panRC) 483 self.btnExport = pack_button('Export...', self.panRC, on_click=self._onPressExport) 484 self.btnImport = pack_button('Import...', self.panRC, on_click=self._onPressImport) 485 self.panLoad = pack_item(gtk.HBox(), h) 486 pack_label('Load:', self.panLoad) 487 self.txtLoadFrom = pack_item(NumEntry(1, acceptIntGreaterThan(-2), width_chars=4), self.panLoad) 488 pack_label('-', self.panLoad, expand=True) 489 self.txtLoadTo = pack_item(NumEntry(1, acceptIntGreaterThan(-2), width_chars=4), self.panLoad) 490 self.txtData = gtk.TextView() 491 self.txtDataOut = TextViewAppender(self.txtData) 492 pack_scrolled(self.txtData, self, expand=True) 493 494 self.buttons = pack_item(gtk.HBox(), self) 495 self.btnApply = pack_button('Apply', self.buttons, at_end=True, sensitive=False)
496
497 - def set_node(self, node):
498 self._node = node 499 self.refresh()
500 node = property(lambda s: s._node, set_node)
501 - def refresh(self):
502 node = self.node 503 if (node is None) or node.isNull: 504 self.header.set_sensitive(False) 505 self.txtData.set_sensitive(False) 506 self.header2.hide_all() 507 self.panLoad.hide_all() 508 else: 509 self.header.set_sensitive(True) 510 self.header2.show_all() 511 self.txtName.set_editable((not node.path) or (not node.parent.isNull)) 512 self.txtName.value = node.name 513 self.mnuType.choose(qubx.tree.TYPENAMES[node.data.type]) 514 self.chkPreload.set_active(node.data.preload) 515 self.txtData.set_sensitive(bool(node.data)) 516 if node.data.type == qubx.tree.QTR_TYPE_EMPTY: 517 self.panRC.hide_all() 518 self.panLoad.hide_all() 519 self.txtData.get_buffer().set_text("") 520 elif node.data.type in [qubx.tree.QTR_TYPE_UNKNOWN, qubx.tree.QTR_TYPE_POINTER]: 521 self.panRC.hide_all() 522 self.panLoad.hide_all() 523 self.txtData.get_buffer().set_text("Can't edit this data type; choose Data-type:empty to clear.") 524 elif node.data.type == qubx.tree.QTR_TYPE_STRING: 525 self.panRC.hide_all() 526 self.panLoad.hide_all() 527 self.txtData.get_buffer().set_text(str(node.data)) 528 else: # numeric 529 self.panRC.show_all() 530 self.txtRows.value = node.data.rows 531 self.txtCols.value = node.data.cols 532 if node.data.preload: 533 self.panLoad.hide_all() 534 node.data.loadRows() 535 if node.data.count == 1: 536 lst = node.data[0] 537 elif node.data.cols == 1: 538 lst = node.data[:] 539 else: 540 lst = [node.data.row(i)[0:node.data.cols] for i in xrange(node.data.rows)] 541 else: 542 la, lb = node.data.loadedRows 543 self.panLoad.show_all() 544 self.txtLoadFrom.value = la 545 self.txtLoadTo.value = lb 546 if la == -1: 547 lst = [] 548 else: 549 lst = [node.data.row(i)[0:node.data.cols] for i in xrange(la, lb+1)] 550 self.txtData.get_buffer().set_text("") 551 pprint(lst, self.txtDataOut) 552 self.txtData.modify_text(gtk.STATE_NORMAL, gdk.color_parse("#000000"))
553 - def reshape(self, type, rows, cols):
554 node = self.node 555 if type == qubx.tree.QTR_TYPE_EMPTY: 556 node.data = None 557 return True 558 elif type in [qubx.tree.QTR_TYPE_UNKNOWN, qubx.tree.QTR_TYPE_POINTER]: 559 gobject.idle_add(self.mnuType.choose, qubx.tree.TYPENAMES[node.data.type]) 560 return False 561 elif type == qubx.tree.QTR_TYPE_STRING: 562 buf = self.txtData.get_buffer() 563 node.data = buf.get_text(buf.get_start_iter(), buf.get_end_iter()) or "edit_me" 564 return True 565 else: 566 if node.data.type in [qubx.tree.QTR_TYPE_EMPTY, qubx.tree.QTR_TYPE_UNKNOWN, qubx.tree.QTR_TYPE_POINTER]: 567 node.data.setup(type, 1, 1) 568 node.data[0] = 0 569 elif node.data.type == qubx.tree.QTR_TYPE_STRING: 570 node.data.setup(type, 1, 1) 571 self.reparse() 572 elif (node.data.type == type) and (node.data.cols == cols): 573 node.data.resize(rows) 574 else: 575 clone = node.clone(False) 576 pre = node.data.preload 577 ldb = node.data.loadedRows 578 node.data.preload = True 579 node.data.setup(type, rows, cols) 580 for i in xrange(min(rows, node.data.rows)): 581 node_row = node.data.row(i) 582 clone_row = clone.data.row(i) 583 for j in xrange(min(len(node_row), len(clone_row))): 584 node_row[j] = clone_row[j] 585 node.data.preload = pre 586 if pre: 587 node.data.loadRows() 588 else: 589 node.data.loadRows(*ldb) 590 return True
591 - def reparse(self):
592 node = self.node 593 buf = self.txtData.get_buffer() 594 txt = buf.get_text(buf.get_start_iter(), buf.get_end_iter()) 595 if node.data.type == qubx.tree.QTR_TYPE_STRING: 596 node.data = txt 597 self.btnApply.set_sensitive(False) 598 self.txtData.modify_text(gtk.STATE_NORMAL, gdk.color_parse("#000000")) 599 else: 600 try: 601 lst = acceptNumList(txt or '[]') 602 try: # make uniform 2d: 603 sum(lst) 604 lst = [[x] for x in lst] 605 except: 606 pass 607 la, lb = node.data.loadedRows 608 if la == -1: 609 return 610 if node.data.preload: 611 self.reshape(node.data.type, len(lst), len(lst) and len(lst[0])) 612 la, lb = node.data.loadedRows 613 for i in xrange(min(len(lst), lb-la+1)): 614 lst_row = lst[i] 615 node_row = node.data.row(la+i) 616 for j in xrange(min(node.data.cols, len(lst_row))): 617 node_row[j] = lst_row[j] 618 self.txtData.modify_text(gtk.STATE_NORMAL, gdk.color_parse("#000000")) 619 self.btnApply.set_sensitive(False) 620 except: 621 traceback.print_exc()
622 - def _onChangeName(self, txt, val):
623 self.node.name = val
624 - def _onPopulateType(self, add):
625 for t in qubx.tree.TYPES: 626 add(qubx.tree.TYPENAMES[t])
627 - def _onChangeType(self, mnu, val):
628 self.reshape(self.mnuType.active_i, max(1, self.node.data.rows), max(1, self.node.data.cols)) 629 self.refresh()
630 - def _onTogglePreload(self, chk):
631 self.node.data.preload = chk.get_active() 632 self.refresh()
633 - def _onChangeRows(self, txt, val):
634 self.reshape(self.node.data.type, val, self.node.data.cols) 635 self.refresh()
636 - def _onChangeCols(self, txt, val):
637 self.reshape(self.node.data.type, self.node.data.rows, val) 638 self.refresh()
639 - def _onPressExport(self, btn):
640 dlg = gtk.FileChooserDialog('Export data as...', None, gtk.FILE_CHOOSER_ACTION_SAVE, 641 buttons=(gtk.STOCK_CANCEL,gtk.RESPONSE_CANCEL,gtk.STOCK_SAVE,gtk.RESPONSE_OK)) 642 dlg.set_default_response(gtk.RESPONSE_OK) 643 filter = gtk.FileFilter() 644 filter.set_name("Comma-separated files") 645 filter.add_pattern("*.csv") 646 dlg.add_filter(filter) 647 response = dlg.run() 648 if response == gtk.RESPONSE_OK: 649 try: 650 fi = open(dlg.get_filename(), "w") 651 self._export(fi) 652 except Exception, e: 653 print "Saving %s: %s" % (dlg.get_filename(), e) 654 dlg.destroy()
655 - def _onPressImport(self, btn):
656 dlg = gtk.FileChooserDialog('Import data...', None, gtk.FILE_CHOOSER_ACTION_OPEN, 657 buttons=(gtk.STOCK_CANCEL,gtk.RESPONSE_CANCEL,gtk.STOCK_OPEN,gtk.RESPONSE_OK)) 658 dlg.set_default_response(gtk.RESPONSE_OK) 659 filter = gtk.FileFilter() 660 filter.set_name("Comma-separated files") 661 filter.add_pattern("*.csv") 662 dlg.add_filter(filter) 663 filter = gtk.FileFilter() 664 filter.set_name("All files") 665 filter.add_pattern("*") 666 dlg.add_filter(filter) 667 response = dlg.run() 668 if response == gtk.RESPONSE_OK: 669 try: 670 self._import(open(dlg.get_filename(), "r")) 671 except: 672 traceback.print_exc() 673 dlg.destroy()
674 - def _export(self, fi):
675 node = self.node 676 data = node.data 677 la, lb = node.data.loadedRows 678 if la == 0 and lb == data.rows-1: 679 for i in xrange(data.rows): 680 fi.write(','.join(map(str, data.row(i)[0:data.cols]))) 681 fi.write('\n') 682 else: 683 atatime = 1000000 / node.data.size 684 for ii in xrange(0, data.rows, atatime): 685 node.data.loadRows(ii,ii+atatime-1) 686 for i in xrange(ii, min(data.rows,ii+atatime)): 687 fi.write(','.join(map(str, data.row(i)[0:data.cols]))) 688 fi.write('\n') 689 if la >= 0: 690 data.loadRows(la, lb) 691 else: 692 data.unloadRows()
693 - def _import(self, fi):
694 self.txtData.get_buffer().set_text("") 695 out = self.txtDataOut 696 out.write('[') 697 first = True 698 for line in fi: 699 if first: 700 first = False 701 else: 702 out.write(',\n') 703 if line: 704 out.write('[%s]' % line.strip()) 705 out.write(']') 706 self.reparse() 707 self.refresh()
708 - def _onChangeLoadFrom(self, txt, val):
709 node = self.node 710 ld_to = node.data.loadedRows[1] 711 if val >= 0 and ld_to < val: 712 ld_to = val 713 if val >= 0: 714 node.data.loadRows(val, ld_to) 715 else: 716 node.data.unloadRows() 717 self.refresh()
718 - def _onChangeLoadTo(self, txt, val):
719 node = self.node 720 ld_from = node.data.loadedRows[0] 721 if val >= 0 and ld_from > val: 722 ld_from = val 723 if val >= 0: 724 node.data.loadRows(ld_from, val) 725 else: 726 node.data.unloadRows() 727 self.refresh()
728 - def _onChangeData(self, buf):
729 self.txtData.modify_text(gtk.STATE_NORMAL, gdk.color_parse("#FF0000")) 730 self.btnApply.set_sensitive(True)
731 - def _onFocusOutData(self, txt, evt):
732 self.reparse() 733 self.refresh()
734 - def _onPressApply(self, btn):
735 self.reparse() 736 self.refresh()
737
738 -class NodeDialog(gtk.Dialog):
739 """A dialog box to hold the NodeView for editing a single qubx.tree.Node."""
740 - def __init__(self, parent=None, appName=""):
741 """ 742 @param parent: a gtk.Window to center the dialog upon 743 @param appName: host application name, for the title bar 744 """ 745 gtk.Dialog.__init__(self, "", parent or qubx.GTK.get_active_window(), gtk.DIALOG_MODAL) 746 appPrefix = appName and (appName+" - ") or "" 747 self.title_prefix = appPrefix+"Edit data" 748 self.set_title(self.title_prefix) 749 750 self.view = pack_item(NodeView(), self.vbox, expand=True) 751 self.add_buttons(gtk.STOCK_CLOSE, gtk.RESPONSE_REJECT)
752 - def run(self, node):
753 """Shows node's properties, modally""" 754 self.view.node = node 755 self.set_title(self.title_prefix + " for "+node.name) 756 return gtk.Dialog.run(self)
757 758 759 ######### top-level window ############### 760
761 -class TreeWindow(gtk.Window):
762 """A top-level gtk.Window with a TreeView for editing a qubx.tree.Node and its children. 763 764 @ivar view: the TreeView 765 @ivar root: view.root 766 """
767 - def __init__(self, stand_alone=False, appName=""):
768 """ 769 @param stand_alone: has menus, prompts to save, quits on close 770 @param appName: host application name, for the title bar 771 """ 772 gtk.Window.__init__(self, gtk.WINDOW_TOPLEVEL) 773 self.__ref = Reffer() 774 self.stand_alone = stand_alone 775 appPrefix = appName and (appName+" - ") or "" 776 self.title_prefix = appPrefix+"Tree Editor" 777 self.set_title(self.title_prefix) 778 self._build_ui() 779 780 self.connect('delete_event', self._onDelete) 781 self.connect('destroy', self._onDestroy) 782 783 self.root = qubx.tree.Node('Untitled')
784
785 - def _build_ui(self):
786 v = gtk.VBox() 787 v.show() 788 if self.stand_alone: 789 self._buildMenus() 790 pack_item(self.menubar, v) 791 self.view = TreeView() 792 pack_scrolled(self.view, v, expand=True) 793 self.add(v)
794
795 - def set_root(self, root):
796 self._root = root 797 self.view.root = root
798 root = property(lambda s: s._root, set_root)
799 - def open(self, path):
800 try: 801 root = qubx.tree.Open(path) 802 if not root.isNull: 803 self.root = root 804 self.set_title(self.title_prefix+" - "+self.get_filename()) 805 except: 806 traceback.print_exc()
807 - def promptSave(self):
808 if self.stand_alone and self.view.changed: 809 filename = self.get_filename() 810 dlg = gtk.MessageDialog(self, buttons=gtk.BUTTONS_NONE, 811 flags=gtk.DIALOG_MODAL, 812 message_format='Save changes to %s (%s)?' % (filename, self.root.name)) 813 dlg.add_buttons('Yes',gtk.RESPONSE_YES, 814 'No',gtk.RESPONSE_NO, 815 'Cancel',gtk.RESPONSE_CANCEL) 816 response = dlg.run() 817 dlg.destroy() 818 if response == gtk.RESPONSE_YES: 819 if not self.save(): return self.promptSave() # ask again on failure 820 elif response == gtk.RESPONSE_CANCEL: 821 return False 822 elif response == gtk.RESPONSE_NO: 823 pass 824 return True
825 - def save(self, ipath=None):
826 try: 827 path = ipath or self.root.path or "" 828 if not path: 829 return self.saveAs() 830 if path == self.root.path: 831 if self.root.save(): 832 self.view.changed = False 833 return True 834 if self.root.saveAs(path): 835 self.view.changed = False 836 return True 837 return False 838 except Exception, e: 839 dlg = gtk.MessageDialog(self, buttons=gtk.BUTTONS_OK, 840 flags=gtk.DIALOG_MODAL, 841 message_format=str(e)) 842 dlg.run() 843 dlg.destroy() 844 return False
845 - def saveAs(self, ipath=None):
846 path = ipath or self.root.path or "" 847 dlg = gtk.FileChooserDialog('Save as...', self, gtk.FILE_CHOOSER_ACTION_SAVE, 848 buttons=(gtk.STOCK_CANCEL,gtk.RESPONSE_CANCEL,gtk.STOCK_SAVE,gtk.RESPONSE_OK)) 849 dlg.set_default_response(gtk.RESPONSE_OK) 850 dlg.set_filename(path) 851 filter = gtk.FileFilter() 852 filter.set_name("all files") 853 filter.add_pattern("*.*") 854 dlg.add_filter(filter) 855 dlg.set_current_folder(self.get_path()) 856 response = dlg.run() 857 if response == gtk.RESPONSE_OK: 858 try: 859 self.root.saveAs(dlg.get_filename()) 860 self.view.changed = False 861 self.set_title(self.title_prefix+" - "+self.get_filename()) 862 except Exception, e: 863 mdlg = gtk.MessageDialog(self, buttons=gtk.BUTTONS_OK, flags=gtk.DIALOG_MODAL, 864 message_format=str(e)) 865 mdlg.run() 866 mdlg.destroy() 867 response = gtk.RESPONSE_CANCEL 868 dlg.destroy() 869 return response == gtk.RESPONSE_OK
870 - def saveAsText(self, ipath=None):
871 path = ipath or self.root.path or "" 872 print path, os.path.splitext(path) 873 path = os.path.splitext(path)[0] + '.txt' 874 print path 875 dlg = gtk.FileChooserDialog('Save as...', self, gtk.FILE_CHOOSER_ACTION_SAVE, 876 buttons=(gtk.STOCK_CANCEL,gtk.RESPONSE_CANCEL,gtk.STOCK_SAVE,gtk.RESPONSE_OK)) 877 dlg.set_default_response(gtk.RESPONSE_OK) 878 dlg.set_filename(path) 879 filter = gtk.FileFilter() 880 filter.set_name("text files") 881 filter.add_pattern("*.txt") 882 dlg.add_filter(filter) 883 dlg.set_current_folder(self.get_path()) 884 response = dlg.run() 885 if response == gtk.RESPONSE_OK: 886 try: 887 path = os.path.splitext(dlg.get_filename())[0] + '.txt' 888 self.root.saveAsText(path) 889 except Exception, e: 890 mdlg = gtk.MessageDialog(self, buttons=gtk.BUTTONS_OK, flags=gtk.DIALOG_MODAL, 891 message_format=str(e)) 892 mdlg.run() 893 mdlg.destroy() 894 response = gtk.RESPONSE_CANCEL 895 dlg.destroy() 896 return response == gtk.RESPONSE_OK
897 - def get_path(self):
898 if self.root.path: 899 return os.path.split(self.root.path)[0] 900 else: 901 return ""
902 - def get_filename(self):
903 if self.root.path: 904 return os.path.split(self.root.path)[1] 905 else: 906 return ""
907 - def _onDelete(self, w, evt):
908 if self.promptSave(): 909 if self.stand_alone: 910 return False # they destroy 911 else: 912 self.hide() 913 return True # we handle
914 - def _onDestroy(self, window):
915 if self.stand_alone: 916 gtk.main_quit()
917 - def _buildMenus(self):
918 self.mnuFile = gtk.Menu() 919 self.itemNew = build_menuitem("New", self.__ref(self._onItemNew), menu=self.mnuFile) 920 self.itemPasteNew = build_menuitem("Paste as new document", self.__ref(self._onItemPasteNew), menu=self.mnuFile) 921 self.itemOpen = build_menuitem("Open...", self.__ref(self._onItemOpen), menu=self.mnuFile) 922 self.itemSave = build_menuitem('Save', self.__ref(self._onItemSave), menu=self.mnuFile) 923 self.itemSaveAs = build_menuitem('Save As...', self.__ref(self._onItemSaveAs), menu=self.mnuFile) 924 self.itemSaveAsText = build_menuitem('Save a copy as text...', self.__ref(self._onItemSaveAsText), menu=self.mnuFile) 925 self.itemQuit = build_menuitem(self.stand_alone and 'Quit' or 'Close', self.__ref(self._onItemQuit), menu=self.mnuFile) 926 927 self.menubar = gtk.MenuBar() 928 self.mnuItemFile = build_menuitem('File', self.__ref(self._onShowMnuFile), submenu=self.mnuFile, menu=self.menubar)
929 - def _onShowMnuFile(self, item):
930 self.itemPasteNew.set_sensitive(CanPaste()) 931 self.itemSave.set_sensitive((not self.root.path) or bool(self.root.modified))
932 - def _onItemNew(self, item):
933 self.root = qubx.tree.Node('Untitled') 934 self.set_title(self.title_prefix)
935 - def _onItemPasteNew(self, item):
936 node = Paste() 937 if node.isNull: return 938 self.root = node 939 self.set_title(self.title_prefix)
940 - def _onItemOpen(self, item):
941 dlg = gtk.FileChooserDialog('Open...', self, gtk.FILE_CHOOSER_ACTION_OPEN, 942 buttons=(gtk.STOCK_CANCEL,gtk.RESPONSE_CANCEL,gtk.STOCK_OPEN,gtk.RESPONSE_OK)) 943 dlg.set_default_response(gtk.RESPONSE_OK) 944 filter = gtk.FileFilter() 945 filter.set_name("All files") 946 filter.add_pattern("*") 947 dlg.add_filter(filter) 948 dlg.set_current_folder(self.get_path()) 949 response = dlg.run() 950 if response == gtk.RESPONSE_OK: 951 self.open(dlg.get_filename()) 952 dlg.destroy()
953 - def _onItemSave(self, item):
954 self.save()
955 - def _onItemSaveAs(self, item):
956 self.saveAs()
957 - def _onItemSaveAsText(self, item):
958 self.saveAsText()
959 - def _onItemQuit(self, item):
960 if self.promptSave(): 961 if self.stand_alone: 962 self.destroy() 963 else: 964 self.hide()
965 966 ######### dialog and function ############### 967
968 -class TreeDialog(gtk.Dialog):
969 """ 970 Dialog for modal editing of a qubx.tree.Node and its children. 971 972 @ivar view: the TreeView 973 @ivar root: view.root 974 """
975 - def __init__(self, parent=None, appName="", caption=""):
976 """ 977 @param parent: a gtk.Window to center the dialog upon 978 @param appName: host application name, for the title bar 979 @param caption: optional text for the top of the dialog 980 """ 981 gtk.Dialog.__init__(self, "", parent, gtk.DIALOG_MODAL) 982 self.resize(500, 600) 983 appPrefix = appName and (appName+" - ") or "" 984 self.title_prefix = appPrefix+"Tree Editor" 985 self.set_title(self.title_prefix) 986 987 if caption: 988 pack_label(caption, self.vbox) 989 self.view = TreeView() 990 pack_scrolled(self.view, self.vbox, expand=True) 991 992 self.root = qubx.tree.Node('Untitled')
993 - def set_root(self, root):
994 self._root = root 995 self.view.root = root
996 root = property(lambda s: s._root, set_root)
997 - def run(self, node):
998 """Shows the dialog modally, editing node""" 999 self.root = node 1000 return gtk.Dialog.run(self)
1001
1002 -def UserEdit(node, parent=None, appName="", caption=""):
1003 """Creates a TreeDialog, runs it, and destroys it; returns the edited node.""" 1004 dlg = TreeDialog(parent, appName, caption) 1005 dlg.run(node) 1006 dlg.destroy() 1007 return node
1008 1009 1010 1011 if __name__ == '__main__': 1012 qubx.tree.CHOOSE_FLAVOR('numpy') 1013 path = os.path.join(os.path.expanduser('~'), '.qubtree') 1014 qubx.pyenv.Init(path) 1015 1016 w = TreeWindow(stand_alone=True) 1017 w.open(sys.argv[1]) 1018 w.show() 1019 1020 gtk.main() 1021 1022 # UserEdit(w.root, caption="This is demonstrating simple UserEdit() dialog style") 1023