46
46
47
47
from qtutils .qt import QtCore , QtGui , QtWidgets
48
48
from qtutils .qt .QtCore import pyqtSignal as Signal
49
- from qtutils import inmain_decorator , UiLoader , DisconnectContextManager
49
+ from qtutils import inmain_decorator , inmain , UiLoader , DisconnectContextManager
50
50
from qtutils .outputbox import OutputBox
51
51
from qtutils .auto_scroll_to_end import set_auto_scroll_to_end
52
52
import qtutils .icons
@@ -1143,6 +1143,8 @@ def __init__(self, view, exp_config):
1143
1143
self ._view = view
1144
1144
self .exp_config = exp_config
1145
1145
self ._model = UneditableModel ()
1146
+ self .row_number_by_filepath = {}
1147
+ self ._previous_n_digits = 0
1146
1148
1147
1149
headerview_style = """
1148
1150
QHeaderView {
@@ -1213,17 +1215,6 @@ def connect_signals(self):
1213
1215
self ._view .customContextMenuRequested .connect (self .on_view_context_menu_requested )
1214
1216
self .action_remove_selected .triggered .connect (self .on_remove_selection )
1215
1217
1216
- def get_model_row_by_filepath (self , filepath ):
1217
- filepath_colname = ('filepath' ,) + ('' ,) * (self .nlevels - 1 )
1218
- possible_items = self ._model .findItems (filepath , column = self .column_indices [filepath_colname ])
1219
- if len (possible_items ) > 1 :
1220
- raise LookupError ('Multiple items found' )
1221
- elif not possible_items :
1222
- raise LookupError ('No item found' )
1223
- item = possible_items [0 ]
1224
- index = item .index ()
1225
- return index .row ()
1226
-
1227
1218
def on_remove_selection (self ):
1228
1219
self .remove_selection ()
1229
1220
@@ -1325,8 +1316,8 @@ def mark_as_deleted_off_disk(self, filepath):
1325
1316
# Shot has been removed from FileBox, nothing to do here:
1326
1317
return
1327
1318
1328
- model_row_number = self .get_model_row_by_filepath ( filepath )
1329
- status_item = self ._model .item (model_row_number , self .COL_STATUS )
1319
+ row_number = self .row_number_by_filepath [ filepath ]
1320
+ status_item = self ._model .item (row_number , self .COL_STATUS )
1330
1321
already_marked_as_deleted = status_item .data (self .ROLE_DELETED_OFF_DISK )
1331
1322
if already_marked_as_deleted :
1332
1323
return
@@ -1350,38 +1341,40 @@ def update_row(self, filepath, dataframe_already_updated=False, status_percent=N
1350
1341
if (new_row_data is None ) == (updated_row_data is None ) and not dataframe_already_updated :
1351
1342
raise ValueError ('Exactly one of new_row_data or updated_row_data must be provided' )
1352
1343
1353
- df_row_index = np .where (self .dataframe ['filepath' ].values == filepath )
1354
1344
try :
1355
- df_row_index = df_row_index [ 0 ][ 0 ]
1356
- except IndexError :
1345
+ row_number = self . row_number_by_filepath [ filepath ]
1346
+ except KeyError :
1357
1347
# Row has been deleted, nothing to do here:
1358
1348
return
1359
1349
1350
+ filepath_colname = ('filepath' ,) + ('' ,) * (self .nlevels - 1 )
1351
+ assert filepath == self .dataframe .get_value (row_number , filepath_colname )
1352
+
1360
1353
if updated_row_data is not None and not dataframe_already_updated :
1361
1354
for group , name in updated_row_data :
1362
1355
column_name = (group , name ) + ('' ,) * (self .nlevels - 2 )
1363
1356
value = updated_row_data [group , name ]
1364
1357
try :
1365
- self .dataframe .set_value (df_row_index , column_name , value )
1358
+ self .dataframe .set_value (row_number , column_name , value )
1366
1359
except ValueError :
1367
1360
# did the column not already exist when we tried to set an iterable?
1368
1361
if not column_name in self .dataframe .columns :
1369
1362
# create it with a non-iterable and then overwrite with the iterable value:
1370
- self .dataframe .set_value (df_row_index , column_name , None )
1363
+ self .dataframe .set_value (row_number , column_name , None )
1371
1364
else :
1372
1365
# Incompatible datatype - convert the datatype of the column to
1373
1366
# 'object'
1374
1367
self .dataframe [column_name ] = self .dataframe [column_name ].astype ('object' )
1375
1368
# Now that the column exists and has dtype object, we can set the value:
1376
- self .dataframe .set_value (df_row_index , column_name , value )
1369
+ self .dataframe .set_value (row_number , column_name , value )
1377
1370
1378
1371
dataframe_already_updated = True
1379
1372
1380
1373
if not dataframe_already_updated :
1381
1374
if new_row_data is None :
1382
1375
raise ValueError ("If dataframe_already_updated is False, then new_row_data, as returned "
1383
1376
"by dataframe_utils.get_dataframe_from_shot(filepath) must be provided." )
1384
- self .dataframe = replace_with_padding (self .dataframe , new_row_data , df_row_index )
1377
+ self .dataframe = replace_with_padding (self .dataframe , new_row_data , row_number )
1385
1378
self .update_column_levels ()
1386
1379
1387
1380
# Check and create necessary new columns in the Qt model:
@@ -1433,20 +1426,13 @@ def update_row(self, filepath, dataframe_already_updated=False, status_percent=N
1433
1426
self .column_indices = {name : index for index , name in self .column_names .items ()}
1434
1427
1435
1428
# Update the data in the Qt model:
1436
- model_row_number = self .get_model_row_by_filepath (filepath )
1437
- dataframe_row = self .dataframe .iloc [df_row_index ].to_dict ()
1429
+ dataframe_row = self .dataframe .iloc [row_number ].to_dict ()
1438
1430
for column_number , column_name in self .column_names .items ():
1439
1431
if not isinstance (column_name , tuple ):
1440
1432
# One of our special columns, does not correspond to a column in the dataframe:
1441
1433
continue
1442
1434
if updated_row_data is not None and column_name not in updated_row_data :
1443
1435
continue
1444
- item = self ._model .item (model_row_number , column_number )
1445
- if item is None :
1446
- # This is the first time we've written a value to this part of the model:
1447
- item = QtGui .QStandardItem ('NaN' )
1448
- item .setData (QtCore .Qt .AlignCenter , QtCore .Qt .TextAlignmentRole )
1449
- self ._model .setItem (model_row_number , column_number , item )
1450
1436
value = dataframe_row [column_name ]
1451
1437
if isinstance (value , float ):
1452
1438
value_str = scientific_notation (value )
@@ -1457,7 +1443,15 @@ def update_row(self, filepath, dataframe_already_updated=False, status_percent=N
1457
1443
short_value_str = lines [0 ] + ' ...'
1458
1444
else :
1459
1445
short_value_str = value_str
1460
- item .setText (short_value_str )
1446
+
1447
+ item = self ._model .item (row_number , column_number )
1448
+ if item is None :
1449
+ # This is the first time we've written a value to this part of the model:
1450
+ item = QtGui .QStandardItem (short_value_str )
1451
+ item .setData (QtCore .Qt .AlignCenter , QtCore .Qt .TextAlignmentRole )
1452
+ self ._model .setItem (row_number , column_number , item )
1453
+ else :
1454
+ item .setText (short_value_str )
1461
1455
item .setToolTip (repr (value ))
1462
1456
1463
1457
for i , column_name in enumerate (sorted (new_column_names )):
@@ -1466,7 +1460,7 @@ def update_row(self, filepath, dataframe_already_updated=False, status_percent=N
1466
1460
self ._view .resizeColumnToContents (column_number )
1467
1461
1468
1462
if status_percent is not None :
1469
- status_item = self ._model .item (model_row_number , self .COL_STATUS )
1463
+ status_item = self ._model .item (row_number , self .COL_STATUS )
1470
1464
status_item .setData (status_percent , self .ROLE_STATUS_PERCENT )
1471
1465
1472
1466
if new_column_names or defunct_column_names :
@@ -1483,15 +1477,29 @@ def new_row(self, filepath):
1483
1477
name_item = QtGui .QStandardItem (filepath )
1484
1478
return [status_item , name_item ]
1485
1479
1486
- def renumber_rows (self ):
1487
- """Add/update row indices - the rows are numbered in simple sequential order
1488
- for easy comparison with the dataframe"""
1480
+ def renumber_rows (self , add_from = 0 ):
1481
+ """Add/update row indices - the rows are numbered in simple sequential
1482
+ order for easy comparison with the dataframe. add_from allows you to
1483
+ only add numbers for new rows from the given index as a performance
1484
+ optimisation, though if the number of digits changes, all rows will
1485
+ still be renumbered. add_from should not be used if rows have been
1486
+ deleted."""
1489
1487
n_digits = len (str (self ._model .rowCount ()))
1490
- print (self ._model .rowCount ())
1491
- for row_number in range (self ._model .rowCount ()):
1488
+ if n_digits != self ._previous_n_digits :
1489
+ # All labels must be updated:
1490
+ add_from = 0
1491
+ self ._previous_n_digits = n_digits
1492
+
1493
+ if add_from == 0 :
1494
+ self .row_number_by_filepath = {}
1495
+
1496
+ for row_number in range (add_from , self ._model .rowCount ()):
1492
1497
vertical_header_item = self ._model .verticalHeaderItem (row_number )
1493
1498
row_number_str = str (row_number ).rjust (n_digits )
1494
1499
vert_header_text = '{}. |' .format (row_number_str )
1500
+ filepath_item = self ._model .item (row_number , self .COL_FILEPATH )
1501
+ filepath = filepath_item .text ()
1502
+ self .row_number_by_filepath [filepath ] = row_number
1495
1503
if self .integer_indexing :
1496
1504
header_cols = ['sequence_index' , 'run number' , 'run repeat' ]
1497
1505
header_strings = []
@@ -1503,8 +1511,6 @@ def renumber_rows(self):
1503
1511
header_strings .append ('----' )
1504
1512
vert_header_text += ' |' .join (header_strings )
1505
1513
else :
1506
- filepath_item = self ._model .item (row_number , self .COL_FILEPATH )
1507
- filepath = filepath_item .text ()
1508
1514
basename = os .path .splitext (os .path .basename (filepath ))[0 ]
1509
1515
vert_header_text += ' ' + basename
1510
1516
vertical_header_item .setText (vert_header_text )
@@ -1518,7 +1524,7 @@ def add_files(self, filepaths, new_row_data):
1518
1524
1519
1525
# Check for duplicates:
1520
1526
for filepath in filepaths :
1521
- if filepath in self .dataframe [ 'filepath' ]. values or filepath in to_add :
1527
+ if filepath in self .row_number_by_filepath or filepath in to_add :
1522
1528
app .output_box .output ('Warning: Ignoring duplicate shot %s\n ' % filepath , red = True )
1523
1529
if new_row_data is not None :
1524
1530
df_row_index = np .where (new_row_data ['filepath' ].values == filepath )
@@ -1529,19 +1535,26 @@ def add_files(self, filepaths, new_row_data):
1529
1535
1530
1536
assert len (new_row_data ) == len (to_add )
1531
1537
1538
+ if to_add :
1539
+ # Update the dataframe:
1540
+ self .dataframe = concat_with_padding (self .dataframe , new_row_data )
1541
+ self .update_column_levels ()
1542
+
1543
+ app .filebox .set_add_shots_progress (None , None , "updating filebox" )
1544
+
1532
1545
for filepath in to_add :
1533
- # Add the new rows to the model:
1546
+ # Add the new rows to the Qt model:
1534
1547
self ._model .appendRow (self .new_row (filepath ))
1535
1548
vert_header_item = QtGui .QStandardItem ('...loading...' )
1536
1549
self ._model .setVerticalHeaderItem (self ._model .rowCount () - 1 , vert_header_item )
1537
1550
self ._view .resizeRowToContents (self ._model .rowCount () - 1 )
1538
1551
1539
- if to_add :
1540
- self . dataframe = concat_with_padding ( self . dataframe , new_row_data )
1541
- self . update_column_levels ()
1542
- for filepath in to_add :
1543
- self .update_row (filepath , dataframe_already_updated = True )
1544
- self . renumber_rows ()
1552
+ self . renumber_rows ( add_from = self . _model . rowCount () - len ( to_add ))
1553
+
1554
+ # Update the Qt model:
1555
+ for filepath in to_add :
1556
+ self .update_row (filepath , dataframe_already_updated = True )
1557
+
1545
1558
1546
1559
@inmain_decorator ()
1547
1560
def get_first_incomplete (self ):
@@ -1667,14 +1680,20 @@ def set_columns_visible(self, columns_visible):
1667
1680
self .shots_model .set_columns_visible (columns_visible )
1668
1681
1669
1682
@inmain_decorator ()
1670
- def set_add_shots_progress (self , completed , total ):
1671
- if completed == total :
1683
+ def set_add_shots_progress (self , completed , total , message ):
1684
+ self .ui .progressBar_add_shots .setFormat ("Adding shots: [{}] %v/%m (%p%)" .format (message ))
1685
+ if completed == total and message is None :
1672
1686
self .ui .progressBar_add_shots .hide ()
1673
1687
else :
1674
- self .ui .progressBar_add_shots .setMaximum (total )
1675
- self .ui .progressBar_add_shots .setValue (completed )
1688
+ if total is not None :
1689
+ self .ui .progressBar_add_shots .setMaximum (total )
1690
+ if completed is not None :
1691
+ self .ui .progressBar_add_shots .setValue (completed )
1676
1692
if self .ui .progressBar_add_shots .isHidden ():
1677
1693
self .ui .progressBar_add_shots .show ()
1694
+ if completed is None and total is None and message is not None :
1695
+ # Ensure a repaint when only the message changes:
1696
+ self .ui .progressBar_add_shots .repaint ()
1678
1697
1679
1698
def incoming_buffer_loop (self ):
1680
1699
"""We use a queue as a buffer for incoming shots. We don't want to hang and not
@@ -1698,19 +1717,21 @@ def incoming_buffer_loop(self):
1698
1717
if self .incoming_queue .qsize () == 0 :
1699
1718
# Wait momentarily in case more arrive so we can batch process them:
1700
1719
time .sleep (0.1 )
1720
+ # Batch process to decrease number of dataframe concatenations:
1721
+ batch_size = len (self .shots_model .dataframe ) // 3 + 1
1701
1722
while True :
1702
1723
try :
1703
1724
filepath = self .incoming_queue .get (False )
1704
1725
except queue .Empty :
1705
1726
break
1706
1727
else :
1707
1728
filepaths .append (filepath )
1708
- if len (filepaths ) >= 5 :
1729
+ if len (filepaths ) >= batch_size :
1709
1730
break
1710
1731
logger .info ('adding:\n %s' % '\n ' .join (filepaths ))
1711
1732
if n_shots_added == 0 :
1712
1733
total_shots = self .incoming_queue .qsize () + len (filepaths )
1713
- self .set_add_shots_progress (1 , total_shots )
1734
+ self .set_add_shots_progress (1 , total_shots , "reading shot files" )
1714
1735
1715
1736
# Remove duplicates from the list (preserving order) in case the
1716
1737
# client sent the same filepath multiple times:
@@ -1728,15 +1749,12 @@ def incoming_buffer_loop(self):
1728
1749
n_shots_added += 1
1729
1750
shots_remaining = self .incoming_queue .qsize ()
1730
1751
total_shots = n_shots_added + shots_remaining + len (filepaths ) - (i + 1 )
1731
- if i != len (filepaths ) - 1 :
1732
- # Leave the last update until after dataframe concatenation.
1733
- # Looks more responsive that way:
1734
- self .set_add_shots_progress (n_shots_added , total_shots )
1752
+ self .set_add_shots_progress (n_shots_added , total_shots , "reading shot files" )
1753
+ self .set_add_shots_progress (n_shots_added , total_shots , "concatenating dataframes" )
1735
1754
if dataframes :
1736
1755
new_row_data = concat_with_padding (* dataframes )
1737
1756
else :
1738
1757
new_row_data = None
1739
- self .set_add_shots_progress (n_shots_added , total_shots )
1740
1758
1741
1759
# Do not add the shots that were not found on disk. Reverse
1742
1760
# loop so that removing an item doesn't change the indices of
@@ -1748,6 +1766,7 @@ def incoming_buffer_loop(self):
1748
1766
# Let the analysis loop know to look for new shots:
1749
1767
self .analysis_pending .set ()
1750
1768
if shots_remaining == 0 :
1769
+ self .set_add_shots_progress (n_shots_added , total_shots , None )
1751
1770
n_shots_added = 0 # reset our counter for the next batch
1752
1771
1753
1772
except Exception :
0 commit comments