Package Camelot :: Package camelot :: Package view :: Package controls :: Module tableview
[frames] | no frames]

Source Code for Module Camelot.camelot.view.controls.tableview

  1  #  ============================================================================
 
  2  #
 
  3  #  Copyright (C) 2007-2008 Conceptive Engineering bvba. All rights reserved.
 
  4  #  www.conceptive.be / project-camelot@conceptive.be
 
  5  #
 
  6  #  This file is part of the Camelot Library.
 
  7  #
 
  8  #  This file may be used under the terms of the GNU General Public
 
  9  #  License version 2.0 as published by the Free Software Foundation
 
 10  #  and appearing in the file LICENSE.GPL included in the packaging of
 
 11  #  this file.  Please review the following information to ensure GNU
 
 12  #  General Public Licensing requirements will be met:
 
 13  #  http://www.trolltech.com/products/qt/opensource.html
 
 14  #
 
 15  #  If you are unsure which license is appropriate for your use, please
 
 16  #  review the following information:
 
 17  #  http://www.trolltech.com/products/qt/licensing.html or contact
 
 18  #  project-camelot@conceptive.be.
 
 19  #
 
 20  #  This file is provided AS IS with NO WARRANTY OF ANY KIND, INCLUDING THE
 
 21  #  WARRANTY OF DESIGN, MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE.
 
 22  #
 
 23  #  For use of this library in commercial applications, please contact
 
 24  #  project-camelot@conceptive.be
 
 25  #
 
 26  #  ============================================================================
 
 27  
 
 28  """Tableview""" 
 29  
 
 30  import logging 
 31  logger = logging.getLogger( 'camelot.view.controls.tableview' ) 
 32  
 
 33  from PyQt4 import QtCore, QtGui 
 34  from PyQt4.QtGui import QSizePolicy 
 35  from PyQt4.QtCore import SIGNAL 
 36  from PyQt4.QtCore import Qt 
 37  
 
 38  from camelot.view.proxy.queryproxy import QueryTableProxy 
 39  from camelot.view.controls.filterlist import filter_changed_signal 
 40  from camelot.view.controls.view import AbstractView 
 41  from camelot.view.controls.user_translatable_label import UserTranslatableLabel 
 42  from camelot.view.model_thread import model_function, gui_function, post 
 43  from camelot.core.utils import ugettext as _ 
 44  
 
 45  from search import SimpleSearchControl 
46 47 -class TableWidget( QtGui.QTableView):
48 """A widget displaying a table, to be used within a TableView""" 49
50 - def __init__( self, parent = None ):
51 QtGui.QTableView.__init__( self, parent ) 52 logger.debug( 'create TableWidget' ) 53 self.setSelectionBehavior( QtGui.QAbstractItemView.SelectRows ) 54 self.setEditTriggers( QtGui.QAbstractItemView.SelectedClicked | QtGui.QAbstractItemView.DoubleClicked ) 55 self.setSizePolicy( QSizePolicy.Expanding, QSizePolicy.Expanding ) 56 # set to false while sorting is not implemented in CollectionProxy 57 self.horizontalHeader().setClickable( True ) 58 self._header_font_required = QtGui.QApplication.font() 59 self._header_font_required.setBold( True ) 60 self._minimal_row_height = QtGui.QFontMetrics(QtGui.QApplication.font()).lineSpacing() + 10 61 self.verticalHeader().setDefaultSectionSize( self._minimal_row_height ) 62 self.connect( self.horizontalHeader(), QtCore.SIGNAL('sectionClicked(int)'), self.horizontal_section_clicked )
63
64 - def horizontal_section_clicked( self, logical_index ):
65 """Update the sorting of the model and the header""" 66 header = self.horizontalHeader() 67 order = Qt.AscendingOrder 68 if not header.isSortIndicatorShown(): 69 header.setSortIndicatorShown( True ) 70 elif header.sortIndicatorSection()==logical_index: 71 # apparently, the sort order on the header is allready switched when the section 72 # was clicked, so there is no need to reverse it 73 order = header.sortIndicatorOrder() 74 header.setSortIndicator( logical_index, order ) 75 self.model().sort( logical_index, order )
76
77 - def setModel( self, model ):
78 QtGui.QTableView.setModel( self, model ) 79 self.connect( self.selectionModel(), SIGNAL( 'currentChanged(const QModelIndex&,const QModelIndex&)' ), self.activated )
80
81 - def activated( self, selectedIndex, previousSelectedIndex ):
82 option = QtGui.QStyleOptionViewItem() 83 newSize = self.itemDelegate( selectedIndex ).sizeHint( option, selectedIndex ) 84 row = selectedIndex.row() 85 if previousSelectedIndex.row() >= 0: 86 oldSize = self.itemDelegate( previousSelectedIndex ).sizeHint( option, selectedIndex ) 87 previousRow = previousSelectedIndex.row() 88 self.setRowHeight( previousRow, oldSize.height() ) 89 self.setRowHeight( row, newSize.height() )
90
91 -class RowsWidget( QtGui.QLabel ):
92 """Widget that is part of the header widget, displaying the number of rows 93 in the table view""" 94 95 _number_of_rows_font = QtGui.QApplication.font() 96
97 - def __init__( self, parent ):
98 QtGui.QLabel.__init__( self, parent ) 99 self.setFont( self._number_of_rows_font )
100
101 - def setNumberOfRows( self, rows ):
102 self.setText( _('(%i rows)')%rows )
103
104 -class HeaderWidget( QtGui.QWidget ):
105 """HeaderWidget for a tableview, containing the title, the search widget, 106 and the number of rows in the table""" 107 108 search_widget = SimpleSearchControl 109 rows_widget = RowsWidget 110 111 _title_font = QtGui.QApplication.font() 112 _title_font.setBold( True ) 113
114 - def __init__( self, parent, admin ):
115 QtGui.QWidget.__init__( self, parent ) 116 self._admin = admin 117 layout = QtGui.QVBoxLayout() 118 widget_layout = QtGui.QHBoxLayout() 119 search = self.search_widget( self ) 120 self.connect(search, SimpleSearchControl.expand_search_options_signal, self.expand_search_options) 121 title = UserTranslatableLabel( admin.get_verbose_name_plural(), self ) 122 title.setFont( self._title_font ) 123 widget_layout.addWidget( title ) 124 widget_layout.addWidget( search ) 125 if self.rows_widget: 126 self.number_of_rows = self.rows_widget( self ) 127 widget_layout.addWidget( self.number_of_rows ) 128 else: 129 self.number_of_rows = None 130 layout.addLayout( widget_layout ) 131 self._expanded_filters_created = False 132 self._expanded_search = QtGui.QWidget() 133 self._expanded_search.hide() 134 layout.addWidget(self._expanded_search) 135 self.setLayout( layout ) 136 self.setSizePolicy( QSizePolicy.Minimum, QSizePolicy.Fixed ) 137 self.setNumberOfRows( 0 ) 138 self.search = search
139
140 - def _fill_expanded_search_options(self, columns):
141 from camelot.view.controls.filter_operator import FilterOperator 142 layout = QtGui.QHBoxLayout() 143 for field, attributes in columns: 144 if 'operators' in attributes and attributes['operators']: 145 widget = FilterOperator( self._admin.entity, field, attributes, self) 146 self.connect( widget, filter_changed_signal, self._filter_changed ) 147 layout.addWidget( widget ) 148 layout.addStretch() 149 self._expanded_search.setLayout( layout ) 150 self._expanded_filters_created = True
151
152 - def _filter_changed(self):
153 self.emit(QtCore.SIGNAL('filters_changed'))
154
155 - def decorate_query(self, query):
156 """Apply expanded filters on the query""" 157 if self._expanded_filters_created: 158 for i in range(self._expanded_search.layout().count()): 159 if self._expanded_search.layout().itemAt(i).widget(): 160 query = self._expanded_search.layout().itemAt(i).widget().decorate_query(query) 161 return query
162
163 - def expand_search_options(self):
164 if self._expanded_search.isHidden(): 165 if not self._expanded_filters_created: 166 post( self._admin.get_columns, self._fill_expanded_search_options ) 167 self._expanded_search.show() 168 else: 169 self._expanded_search.hide()
170 171 @gui_function
172 - def setNumberOfRows( self, rows ):
173 if self.number_of_rows: 174 self.number_of_rows.setNumberOfRows( rows )
175
176 -class TableView( AbstractView ):
177 """A generic tableview widget that puts together some other widgets. The behaviour of this class and 178 the resulting interface can be tuned by specifying specific class attributes which define the underlying 179 widgets used :: 180 181 class MovieRentalTableView(TableView): 182 title_format = 'Grand overview of recent movie rentals' 183 184 The attributes that can be specified are : 185 186 .. attribute:: header_widget 187 188 The widget class to be used as a header in the table view:: 189 190 header_widget = HeaderWidget 191 192 .. attribute:: table_widget 193 194 The widget class used to display a table within the table view :: 195 196 table_widget = TableWidget 197 198 .. attribute:: title_format 199 200 A string used to format the title of the view :: 201 202 title_format = '%(verbose_name_plural)s' 203 204 .. attribute:: table_model 205 206 A class implementing QAbstractTableModel that will be used as a model for the table view :: 207 208 table_model = QueryTableProxy 209 210 - emits the row_selected signal when a row has been selected 211 """ 212 213 header_widget = HeaderWidget 214 table_widget = TableWidget 215 216 # 217 # The proxy class to use 218 # 219 table_model = QueryTableProxy 220 # 221 # Format to use as the window title 222 # 223 title_format = '%(verbose_name_plural)s' 224
225 - def __init__( self, admin, search_text = None, parent = None ):
226 AbstractView.__init__( self, parent ) 227 self.admin = admin 228 post( self.get_title, self.change_title ) 229 widget_layout = QtGui.QVBoxLayout() 230 if self.header_widget: 231 self.header = self.header_widget( self, admin ) 232 widget_layout.addWidget( self.header ) 233 self.connect( self.header.search, SIGNAL( 'search' ), self.startSearch ) 234 self.connect( self.header.search, SIGNAL( 'cancel' ), self.cancelSearch ) 235 if search_text: 236 self.header.search.search( search_text ) 237 else: 238 self.header = None 239 widget_layout.setSpacing( 0 ) 240 widget_layout.setMargin( 0 ) 241 self.splitter = QtGui.QSplitter( self ) 242 widget_layout.addWidget( self.splitter ) 243 table_widget = QtGui.QWidget( self ) 244 filters_widget = QtGui.QWidget( self ) 245 self.table_layout = QtGui.QVBoxLayout() 246 self.table_layout.setSpacing( 0 ) 247 self.table_layout.setMargin( 0 ) 248 self.table = None 249 self.filters_layout = QtGui.QVBoxLayout() 250 self.filters_layout.setSpacing( 0 ) 251 self.filters_layout.setMargin( 0 ) 252 self.filters = None 253 self.actions = None 254 self._table_model = None 255 table_widget.setLayout( self.table_layout ) 256 filters_widget.setLayout( self.filters_layout ) 257 #filters_widget.hide() 258 self.set_admin( admin ) 259 self.splitter.addWidget( table_widget ) 260 self.splitter.addWidget( filters_widget ) 261 self.setLayout( widget_layout ) 262 self.closeAfterValidation = QtCore.SIGNAL( 'closeAfterValidation()' ) 263 self.search_filter = lambda q: q 264 shortcut = QtGui.QShortcut(QtGui.QKeySequence(QtGui.QKeySequence.Find), self) 265 self.connect( shortcut, QtCore.SIGNAL( 'activated()' ), self.activate_search ) 266 if self.header_widget: 267 self.connect( self.header, QtCore.SIGNAL('filters_changed'), self.rebuildQuery ) 268 # give the table widget focus to prevent the header and its search control to 269 # receive default focus, as this would prevent the displaying of 'Search...' in the 270 # search control, but this conflicts with the MDI, resulting in the window not 271 # being active and the menus not to work properly 272 #table_widget.setFocus( QtCore.Qt.OtherFocusReason ) 273 #self.setFocusProxy(table_widget) 274 #self.setFocus( QtCore.Qt.OtherFocusReason ) 275 post( self.admin.get_subclass_tree, self.setSubclassTree )
276
277 - def activate_search(self):
278 self.header.search.setFocus(QtCore.Qt.ShortcutFocusReason)
279 280 @model_function
281 - def get_title( self ):
282 return self.title_format % {'verbose_name_plural':self.admin.get_verbose_name_plural()}
283 284 @gui_function
285 - def setSubclassTree( self, subclasses ):
286 if len( subclasses ) > 0: 287 from inheritance import SubclassTree 288 class_tree = SubclassTree( self.admin, self.splitter ) 289 self.splitter.insertWidget( 0, class_tree ) 290 self.connect( class_tree, SIGNAL( 'subclassClicked' ), self.set_admin )
291
292 - def sectionClicked( self, section ):
293 """emits a row_selected signal""" 294 self.emit( SIGNAL( 'row_selected' ), section )
295
296 - def copy_selected_rows( self ):
297 """Copy the selected rows in this tableview""" 298 logger.debug( 'delete selected rows called' ) 299 if self.table and self._table_model: 300 for row in set( map( lambda x: x.row(), self.table.selectedIndexes() ) ): 301 self._table_model.copy_row( row )
302
303 - def select_all_rows( self ):
304 self.table.selectAll()
305
306 - def create_table_model( self, admin ):
307 """Create a table model for the given admin interface""" 308 return self.table_model( admin, 309 admin.get_query, 310 admin.get_columns )
311
312 - def get_admin(self):
313 return self.admin
314
315 - def get_model(self):
316 return self._table_model
317 318 @gui_function
319 - def set_admin( self, admin ):
320 """Switch to a different subclass, where admin is the admin object of the 321 subclass""" 322 logger.debug('set_admin called') 323 self.admin = admin 324 if self.table: 325 self.disconnect(self._table_model, QtCore.SIGNAL( 'layoutChanged()' ), self.tableLayoutChanged ) 326 self.table_layout.removeWidget(self.table) 327 self.table.deleteLater() 328 self._table_model.deleteLater() 329 self.table = self.table_widget( self.splitter ) 330 self._table_model = self.create_table_model( admin ) 331 self.table.setModel( self._table_model ) 332 self.connect( self.table.verticalHeader(), 333 SIGNAL( 'sectionClicked(int)' ), 334 self.sectionClicked ) 335 self.connect( self._table_model, QtCore.SIGNAL( 'layoutChanged()' ), self.tableLayoutChanged ) 336 self.tableLayoutChanged() 337 self.table_layout.insertWidget( 1, self.table ) 338 339 def get_filters_and_actions(): 340 return ( admin.get_filters(), admin.get_list_actions() )
341 342 post( get_filters_and_actions, self.set_filters_and_actions ) 343 post( admin.get_list_charts, self.setCharts )
344 345 @gui_function
346 - def tableLayoutChanged( self ):
347 logger.debug('tableLayoutChanged') 348 if self.header: 349 self.header.setNumberOfRows( self._table_model.rowCount() ) 350 item_delegate = self._table_model.getItemDelegate() 351 if item_delegate: 352 self.table.setItemDelegate( item_delegate ) 353 for i in range( self._table_model.columnCount() ): 354 self.table.setColumnWidth( i, self._table_model.headerData( i, Qt.Horizontal, Qt.SizeHintRole ).toSize().width() )
355 356 @gui_function
357 - def setCharts( self, charts ):
358 """creates and display charts""" 359 pass
360 # if charts: 361 # 362 # from matplotlib.figure import Figure 363 # from matplotlib.backends.backend_qt4agg import FigureCanvasQTAgg as \ 364 # FigureCanvas 365 # 366 # chart = charts[0] 367 # 368 # def getData(): 369 # """fetches data for chart""" 370 # from sqlalchemy.sql import select, func 371 # from elixir import session 372 # xcol = getattr( self.admin.entity, chart['x'] ) 373 # ycol = getattr( self.admin.entity, chart['y'] ) 374 # session.bind = self.admin.entity.table.metadata.bind 375 # result = session.execute( select( [xcol, func.sum( ycol )] ).group_by( xcol ) ) 376 # summary = result.fetchall() 377 # return [s[0] for s in summary], [s[1] for s in summary] 378 # 379 # class MyMplCanvas( FigureCanvas ): 380 # """Ultimately, this is a QWidget (as well as a FigureCanvasAgg)""" 381 # 382 # def __init__( self, parent = None, width = 5, height = 4, dpi = 100 ): 383 # fig = Figure( figsize = ( width, height ), dpi = dpi, facecolor = 'w' ) 384 # self.axes = fig.add_subplot( 111, axisbg = 'w' ) 385 # # We want the axes cleared every time plot() is called 386 # self.axes.hold( False ) 387 # self.compute_initial_figure() 388 # FigureCanvas.__init__( self, fig ) 389 # self.setParent( parent ) 390 # FigureCanvas.setSizePolicy( self, 391 # QSizePolicy.Expanding, 392 # QSizePolicy.Expanding ) 393 # FigureCanvas.updateGeometry( self ) 394 # 395 # 396 # def compute_initial_figure( self ): 397 # pass 398 # 399 # def setData( data ): 400 # """set chart data""" 401 # 402 # class MyStaticMplCanvas( MyMplCanvas ): 403 # """simple canvas with a sine plot""" 404 # 405 # def compute_initial_figure( self ): 406 # """computes initial figure""" 407 # x, y = data 408 # bar_positions = [i - 0.25 for i in range( 1, len( x ) + 1 )] 409 # width = 0.5 410 # self.axes.bar( bar_positions, y, width, color = 'b' ) 411 # self.axes.set_xlabel( 'Year' ) 412 # self.axes.set_ylabel( 'Sales' ) 413 # self.axes.set_xticks( range( len( x ) + 1 ) ) 414 # self.axes.set_xticklabels( [''] + [str( d ) for d in x] ) 415 # 416 # sc = MyStaticMplCanvas( self, width = 5, height = 4, dpi = 100 ) 417 # self.table_layout.addWidget( sc ) 418 # 419 # self.admin.mt.post( getData, setData ) 420
421 - def deleteSelectedRows( self ):
422 """delete the selected rows in this tableview""" 423 logger.debug( 'delete selected rows called' ) 424 confirmation_message = self.admin.get_confirm_delete() 425 confirmed = True 426 if confirmation_message: 427 if QtGui.QMessageBox.question(self, 428 _('Please confirm'), 429 unicode(confirmation_message), 430 QtGui.QMessageBox.Yes, 431 QtGui.QMessageBox.No) == QtGui.QMessageBox.No: 432 confirmed = False 433 if confirmed: 434 for row in set( map( lambda x: x.row(), self.table.selectedIndexes() ) ): 435 self._table_model.removeRow( row )
436 437 @gui_function
438 - def newRow( self ):
439 """Create a new row in the tableview""" 440 from camelot.view.workspace import get_workspace 441 workspace = get_workspace() 442 form = self.admin.create_new_view( workspace, 443 oncreate = lambda o:self._table_model.insertEntityInstance( 0, o ), 444 onexpunge = lambda o:self._table_model.removeEntityInstance( o ) ) 445 workspace.addSubWindow( form ) 446 form.show()
447
448 - def closeEvent( self, event ):
449 """reimplements close event""" 450 logger.debug( 'tableview closed' ) 451 # remove all references we hold, to enable proper garbage collection 452 del self.table_layout 453 del self.table 454 del self.filters 455 del self._table_model 456 event.accept()
457
458 - def selectTableRow( self, row ):
459 """selects the specified row""" 460 self.table.selectRow( row )
461
462 - def makeImport(self):
463 pass
464 # for row in data: 465 # o = self.admin.entity() 466 # #For example, setattr(x, 'foobar', 123) is equivalent to x.foobar = 123 467 # # if you want to import all attributes, you must link them to other objects 468 # #for example: a movie has a director, this isn't a primitive like a string 469 # # but a object fetched from the db 470 # setattr(o, object_attributes[0], row[0]) 471 # name = row[2].split( ' ' ) #director 472 # o.short_description = "korte beschrijving" 473 # o.genre = "" 474 # from sqlalchemy.orm.session import Session 475 # Session.object_session(o).flush([o]) 476 # 477 # post( makeImport ) 478
479 - def selectedTableIndexes( self ):
480 """returns a list of selected rows indexes""" 481 return self.table.selectedIndexes()
482
483 - def getColumns( self ):
484 """return the columns to be displayed in the table view""" 485 return self.admin.get_columns()
486
487 - def getData( self ):
488 """generator for data queried by table model""" 489 for d in self._table_model.getData(): 490 yield d
491
492 - def getTitle( self ):
493 """return the name of the entity managed by the admin attribute""" 494 return self.admin.get_verbose_name()
495
496 - def viewFirst( self ):
497 """selects first row""" 498 self.selectTableRow( 0 )
499
500 - def viewLast( self ):
501 """selects last row""" 502 self.selectTableRow( self._table_model.rowCount() - 1 )
503
504 - def viewNext( self ):
505 """selects next row""" 506 first = self.selectedTableIndexes()[0] 507 next = ( first.row() + 1 ) % self._table_model.rowCount() 508 self.selectTableRow( next )
509
510 - def viewPrevious( self ):
511 """selects previous row""" 512 first = self.selectedTableIndexes()[0] 513 prev = ( first.row() - 1 ) % self._table_model.rowCount() 514 self.selectTableRow( prev )
515
516 - def _set_query(self, query_getter):
517 self._table_model.setQuery(query_getter) 518 self.table.clearSelection()
519
520 - def rebuildQuery( self ):
521 """resets the table model query""" 522 523 def rebuild_query(): 524 query = self.admin.entity.query 525 query = self.header.decorate_query(query) 526 if self.filters: 527 query = self.filters.decorate_query( query ) 528 if self.search_filter: 529 query = self.search_filter( query ) 530 query_getter = lambda:query 531 return query_getter
532 533 post( rebuild_query, self._set_query ) 534
535 - def startSearch( self, text ):
536 """rebuilds query based on filtering text""" 537 from camelot.view.search import create_entity_search_query_decorator 538 logger.debug( 'search %s' % text ) 539 self.search_filter = create_entity_search_query_decorator( self.admin, text ) 540 self.rebuildQuery()
541
542 - def cancelSearch( self ):
543 """resets search filtering to default""" 544 logger.debug( 'cancel search' ) 545 self.search_filter = lambda q: q 546 self.rebuildQuery()
547
548 - def get_selection_getter(self):
549 """:return: a function that returns all the objects corresponging to the selected rows in the 550 table """ 551 552 def selection_getter(): 553 selection = [] 554 for row in set( map( lambda x: x.row(), self.table.selectedIndexes() ) ): 555 selection.append( self._table_model._get_object(row) ) 556 return selection
557 558 return selection_getter 559 560 @gui_function
561 - def set_filters_and_actions( self, filters_and_actions ):
562 """sets filters for the tableview""" 563 filters, actions = filters_and_actions 564 from filterlist import FilterList 565 from actionsbox import ActionsBox 566 logger.debug( 'setting filters for tableview' ) 567 568 if self.filters: 569 self.disconnect( self.filters, SIGNAL( 'filters_changed' ), self.rebuildQuery ) 570 self.filters_layout.removeWidget(self.filters) 571 self.filters.deleteLater() 572 self.filters = None 573 if self.actions: 574 self.filters_layout.removeWidget(self.actions) 575 self.actions.deleteLater() 576 self.actions = None 577 if filters: 578 self.filters = FilterList( filters, parent=self.splitter ) 579 self.filters_layout.addWidget( self.filters ) 580 self.connect( self.filters, SIGNAL( 'filters_changed' ), self.rebuildQuery ) 581 # 582 # filters might have default values, so we need to rebuild the queries 583 # 584 self.rebuildQuery() 585 if actions: 586 selection_getter = self.get_selection_getter() 587 self.actions = ActionsBox( self, 588 self._table_model.get_collection_getter(), 589 selection_getter ) 590 591 self.actions.setActions( actions ) 592 self.filters_layout.addWidget( self.actions )
593
594 - def to_html( self ):
595 """generates html of the table""" 596 table = [[getattr( row, col[0] ) for col in self.admin.get_columns()] 597 for row in self.admin.entity.query.all()] 598 context = { 599 'title': self.admin.get_verbose_name_plural(), 600 'table': table, 601 'columns': [field_attributes['name'] for _field, field_attributes in self.admin.get_columns()], 602 } 603 from camelot.view.templates import loader 604 from jinja import Environment 605 env = Environment( loader = loader ) 606 tp = env.get_template( 'table_view.html' ) 607 return tp.render( context )
608
609 - def importFromFile( self ):
610 """"import data : the data will be imported in the activeMdiChild """ 611 logger.info( 'call import method' ) 612 from camelot.view.wizard.importwizard import ImportWizard 613 wizard = ImportWizard(self, self.admin) 614 wizard.exec_()
615