Home | Trees | Indices | Help |
|
---|
|
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 SimpleSearchControl48 """A widget displaying a table, to be used within a TableView""" 499051 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 )6365 """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 )7678 QtGui.QTableView.setModel( self, model ) 79 self.connect( self.selectionModel(), SIGNAL( 'currentChanged(const QModelIndex&,const QModelIndex&)' ), self.activated )8082 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() )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 100103102 self.setText( _('(%i rows)')%rows )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 ) 113175115 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 = search139141 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 = True151 154156 """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 query162164 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_function177 """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' 224344 345 @gui_function226 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 )276278 self.header.search.setFocus(QtCore.Qt.ShortcutFocusReason)279 280 @model_function 283 284 @gui_function286 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 295297 """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 )302304 self.table.selectAll()305307 """Create a table model for the given admin interface""" 308 return self.table_model( admin, 309 admin.get_query, 310 admin.get_columns )311313 return self.admin314 317 318 @gui_function320 """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 )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 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 ) 420422 """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_function439 """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()447449 """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 461 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 482 486488 """generator for data queried by table model""" 489 for d in self._table_model.getData(): 490 yield d491493 """return the name of the entity managed by the admin attribute""" 494 return self.admin.get_verbose_name()495 499 503505 """selects next row""" 506 first = self.selectedTableIndexes()[0] 507 next = ( first.row() + 1 ) % self._table_model.rowCount() 508 self.selectTableRow( next )509511 """selects previous row""" 512 first = self.selectedTableIndexes()[0] 513 prev = ( first.row() - 1 ) % self._table_model.rowCount() 514 self.selectTableRow( prev )515 519521 """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_getter532 533 post( rebuild_query, self._set_query ) 534536 """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()541543 """resets search filtering to default""" 544 logger.debug( 'cancel search' ) 545 self.search_filter = lambda q: q 546 self.rebuildQuery()547549 """: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 selection557 558 return selection_getter 559 560 @gui_function562 """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 )593595 """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 )608610 """"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
Home | Trees | Indices | Help |
|
---|
Generated by Epydoc 3.0.1 on Sat Jun 12 15:42:09 2010 | http://epydoc.sourceforge.net |