Skip to content

Commit 0fbbbcc

Browse files
ui: improved rows selection (3)
- Pressing the Space key on a view paginates the view forward. - Apply rule actions both to visible selections (the ones the user sees), and non-visible selections (those out of the viewport). - Right clicking on a selection does not discard the selection. - Left clicking on a selection discards the selection and selects the clicked row. - Left clicking on a selected row selects/deselects the row. ref: #1291
1 parent b113ed0 commit 0fbbbcc

File tree

3 files changed

+83
-64
lines changed

3 files changed

+83
-64
lines changed

ui/opensnitch/customwidgets/firewalltableview.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -298,7 +298,7 @@ def refresh(self):
298298
def clearSelection(self):
299299
pass
300300

301-
def copySelection(self):
301+
def selectedRows(self):
302302
selection = self.selectedIndexes()
303303
if not selection:
304304
return None

ui/opensnitch/customwidgets/generictableview.py

Lines changed: 43 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -213,7 +213,7 @@ def __init__(self, parent):
213213
self.mousePressed = False
214214
self.shiftPressed = False
215215
self.ctrlPressed = False
216-
self.selectedRows = {}
216+
self._rows_selection = {}
217217
self.trackingCol = 0
218218

219219
#eventFilter to catch key up/down events and wheel events
@@ -275,12 +275,17 @@ def scrollViewport(self, row):
275275
self.vScrollBar.setValue(pos)
276276
return pos
277277

278+
return None
279+
278280
def mouseReleaseEvent(self, event):
279281
super().mouseReleaseEvent(event)
280282
self.mousePressed = False
283+
if event.button() != Qt.LeftButton:
284+
return
285+
281286
for idx in self.selectionModel().selectedRows(self.trackingCol):
282-
if idx.data() not in self.selectedRows.keys():
283-
self.selectedRows[idx.data()] = self.getRowCells(idx.row())
287+
if idx.data() != None and idx.data() not in self._rows_selection.keys():
288+
self._rows_selection[idx.data()] = self.getRowCells(idx.row())
284289

285290
# TODO: handle selection ranges when Shift is pressed
286291
self._selectSavedIndex()
@@ -294,7 +299,7 @@ def mouseMoveEvent(self, event):
294299
if item == None:
295300
return
296301

297-
clickedItem = self.model().index(item.row(), self.trackingCol)
302+
clickedItem = self.model().index(row, self.trackingCol)
298303
if clickedItem.data() == None:
299304
return
300305
self.handleMouseMoveEvent(row, clickedItem, self.selectionModel().isRowSelected(row))
@@ -305,10 +310,10 @@ def mouseMoveEvent(self, event):
305310

306311
def handleMouseMoveEvent(self, row, clickedItem, selected):
307312
if not selected:
308-
if clickedItem.data() in self.selectedRows.keys():
309-
del self.selectedRows[clickedItem.data()]
313+
if clickedItem.data() in self._rows_selection.keys():
314+
del self._rows_selection[clickedItem.data()]
310315
else:
311-
self.selectedRows[clickedItem.data()] = self.getRowCells(row)
316+
self._rows_selection[clickedItem.data()] = self.getRowCells(row)
312317

313318
# handle scrolling the view while dragging the mouse.
314319
if self.mousePressed:
@@ -319,15 +324,14 @@ def handleMouseMoveEvent(self, row, clickedItem, selected):
319324
nextItem = self.model().index(scrollPos, self.trackingCol)
320325
if nextItem == None or nextItem.data() == None:
321326
return
322-
if clickedItem.data() not in self.selectedRows.keys():
323-
self.selectedRows[nextItem.data()] = self.getRowCells(nextItem.row())
327+
if clickedItem.data() not in self._rows_selection.keys():
328+
self._rows_selection[nextItem.data()] = self.getRowCells(nextItem.row())
324329

325330
# save the selected index, to preserve selection when moving around.
326331
def mousePressEvent(self, event):
327332
# we need to call upper class to paint selections properly
328333
super().mousePressEvent(event)
329-
if event.button() != Qt.LeftButton:
330-
return
334+
rightBtnPressed = event.button() != Qt.LeftButton
331335

332336
pos = event.pos()
333337
item = self.indexAt(pos)
@@ -339,7 +343,7 @@ def mousePressEvent(self, event):
339343
if clickedItem.data() == None:
340344
return
341345

342-
self.mousePressed = True
346+
self.mousePressed = not rightBtnPressed
343347
flags = QItemSelectionModel.Rows | QItemSelectionModel.SelectCurrent
344348

345349
# 1. if ctrl is pressed, select / deselect current row
@@ -349,20 +353,21 @@ def mousePressEvent(self, event):
349353
# 3. if ctrl is not pressed and there's more than one row selected, and
350354
# the clicked row is selected: discard selection, and select current
351355
# clicked row.
352-
rowSelected = clickedItem.data() in self.selectedRows.keys()
356+
# 4. if Left button has not been pressed, do not discard the selection.
357+
rowSelected = clickedItem.data() in self._rows_selection.keys()
353358
if self.ctrlPressed:
354359
if rowSelected:
355-
del self.selectedRows[clickedItem.data()]
360+
del self._rows_selection[clickedItem.data()]
356361
flags = QItemSelectionModel.Rows | QItemSelectionModel.Deselect
357362
else:
358-
deselectCurRow = len(self.selectedRows.keys()) == 1
359-
self.selectionModel().clear()
360-
self.selectedRows = {}
361-
if rowSelected and deselectCurRow:
362-
#del self.selectedRows[clickedItem.data()]
363+
deselectCurRow = len(self._rows_selection.keys()) == 1
364+
if not rightBtnPressed:
365+
self.selectionModel().clear()
366+
self._rows_selection = {}
367+
if rowSelected and deselectCurRow and not rightBtnPressed:
363368
flags = QItemSelectionModel.Rows | QItemSelectionModel.Deselect
364369
else:
365-
self.selectedRows[clickedItem.data()] = self.getRowCells(row)
370+
self._rows_selection[clickedItem.data()] = self.getRowCells(row)
366371

367372
self.selectionModel().setCurrentIndex(
368373
clickedItem,
@@ -400,16 +405,16 @@ def clearSelection(self):
400405
self.selectionModel().reset()
401406
self.selectionModel().clearCurrentIndex()
402407

403-
def copySelection(self):
408+
def selectedRows(self, limit=""):
404409
model = self.selectionModel()
405410
curModel = self.model()
406411
selection = model.selectedRows()
407412
if not selection:
408413
return None
409414

410415
rows = []
411-
for k in self.selectedRows:
412-
rows.append(self.selectedRows[k])
416+
for k in self._rows_selection:
417+
rows.append(self._rows_selection[k])
413418
return rows
414419

415420
def getCurrentIndex(self):
@@ -428,7 +433,7 @@ def selectItem(self, _data, _column):
428433
def _selectSavedIndex(self):
429434
sel = QItemSelection()
430435

431-
for text in self.selectedRows.keys():
436+
for text in self._rows_selection.keys():
432437
items = self.model().findItems(text, column=self.trackingCol)
433438
if len(items) == 0:
434439
continue
@@ -457,8 +462,8 @@ def onScrollbarValueChanged(self, vSBNewValue):
457462
def onKeyUp(self):
458463
curIdx = self.selectionModel().currentIndex()
459464
if not self.shiftPressed:
460-
self.selectedRows = {}
461-
self.selectedRows[curIdx.data()] = self.getRowCells(curIdx.row())
465+
self._rows_selection = {}
466+
self._rows_selection[curIdx.data()] = self.getRowCells(curIdx.row())
462467

463468
if self.selectionModel().currentIndex().row() == 0:
464469
self.vScrollBar.setValue(max(0, self.vScrollBar.value() - 1))
@@ -467,8 +472,8 @@ def onKeyDown(self):
467472
curIdx = self.selectionModel().currentIndex()
468473
curRow = curIdx.row()
469474
if not self.shiftPressed:
470-
self.selectedRows = {}
471-
self.selectedRows[curIdx.data()] = self.getRowCells(curRow)
475+
self._rows_selection = {}
476+
self._rows_selection[curIdx.data()] = self.getRowCells(curRow)
472477

473478
if curRow >= self.maxRowsInViewport-2:
474479
self.onKeyPageDown()
@@ -498,6 +503,13 @@ def onKeyPageDown(self):
498503
newValue = self.vScrollBar.value() + (self.maxRowsInViewport-2)
499504
self.vScrollBar.setValue(newValue)
500505

506+
def onKeySpace(self):
507+
if self.vScrollBar.isVisible() == False:
508+
return
509+
510+
newValue = self.vScrollBar.value() + (self.maxRowsInViewport-2)
511+
self.vScrollBar.setValue(newValue)
512+
501513
def eventFilter(self, obj, event):
502514
if event.type() == QEvent.KeyRelease:
503515
if event.key() == Qt.Key_Shift:
@@ -522,11 +534,13 @@ def eventFilter(self, obj, event):
522534
self.onKeyPageDown()
523535
elif event.key() == Qt.Key_Escape:
524536
self.selectionModel().clear()
525-
self.selectedRows = {}
537+
self._rows_selection = {}
526538
elif event.key() == Qt.Key_Shift:
527539
self.shiftPressed = True
528540
elif event.key() == Qt.Key_Control:
529541
self.ctrlPressed = True
542+
elif event.key() == Qt.Key_Space:
543+
self.onKeySpace()
530544

531545
elif event.type() == QEvent.Wheel:
532546
self.vScrollBar.wheelEvent(event)

ui/opensnitch/dialogs/stats.py

Lines changed: 39 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1202,9 +1202,7 @@ def _configure_rules_contextual_menu(self, pos):
12021202
table = self._get_active_table()
12031203
model = table.model()
12041204

1205-
selection = table.selectionModel().selectedRows()
1206-
if not selection:
1207-
return False
1205+
selection = table.selectedRows()
12081206

12091207
menu = QtWidgets.QMenu()
12101208
durMenu = QtWidgets.QMenu(self.COL_STR_DURATION)
@@ -1231,12 +1229,18 @@ def _configure_rules_contextual_menu(self, pos):
12311229
_dur5m = durMenu.addAction(Config.DURATION_5m)
12321230
menu.addMenu(durMenu)
12331231

1234-
is_rule_enabled = model.index(selection[0].row(), self.COL_R_ENABLED).data()
1235-
menu_label_enable = QC.translate("stats", "Disable")
1236-
if is_rule_enabled == "False":
1237-
menu_label_enable = QC.translate("stats", "Enable")
1232+
is_rule_enabled = True
1233+
_menu_enable = None
1234+
# if there's more than one rule selected, we choose an action
1235+
# based on the status of the first rule.
1236+
if selection and len(selection) > 0:
1237+
is_rule_enabled = selection[0][self.COL_R_ENABLED]
1238+
menu_label_enable = QC.translate("stats", "Disable")
1239+
if is_rule_enabled == "False":
1240+
menu_label_enable = QC.translate("stats", "Enable")
1241+
1242+
_menu_enable = menu.addAction(QC.translate("stats", menu_label_enable))
12381243

1239-
_menu_enable = menu.addAction(QC.translate("stats", menu_label_enable))
12401244
_menu_duplicate = menu.addAction(QC.translate("stats", "Duplicate"))
12411245
_menu_edit = menu.addAction(QC.translate("stats", "Edit"))
12421246
_menu_delete = menu.addAction(QC.translate("stats", "Delete"))
@@ -1365,9 +1369,9 @@ def _table_menu_export_clipboard(self, cur_idx, model, selection):
13651369
rules_list.append(self._fw.rule_to_json(r))
13661370

13671371
elif cur_idx == self.TAB_RULES and self.rulesTable.isVisible():
1368-
for idx in selection:
1369-
rule_name = model.index(idx.row(), self.COL_R_NAME).data()
1370-
node_addr = model.index(idx.row(), self.COL_R_NODE).data()
1372+
for row in selection:
1373+
rule_name = row[self.COL_R_NAME]
1374+
node_addr = row[self.COL_R_NODE]
13711375

13721376
json_rule = self._nodes.rule_to_json(node_addr, rule_name)
13731377
if json_rule != None:
@@ -1393,17 +1397,19 @@ def _table_menu_export_clipboard(self, cur_idx, model, selection):
13931397
QtWidgets.qApp.clipboard().setText(cliptext)
13941398

13951399
def _table_menu_export_disk(self, cur_idx, model, selection):
1396-
outdir = QtWidgets.QFileDialog.getExistingDirectory(self,
1397-
os.path.expanduser("~"),
1398-
QC.translate("stats", 'Select a directory to export rules'),
1399-
QtWidgets.QFileDialog.ShowDirsOnly | QtWidgets.QFileDialog.DontResolveSymlinks)
1400+
outdir = QtWidgets.QFileDialog.getExistingDirectory(
1401+
self,
1402+
os.path.expanduser("~"),
1403+
QC.translate("stats", 'Select a directory to export rules'),
1404+
QtWidgets.QFileDialog.ShowDirsOnly | QtWidgets.QFileDialog.DontResolveSymlinks
1405+
)
14001406
if outdir == "":
14011407
return
14021408

14031409
error_list = []
1404-
for idx in selection:
1405-
rule_name = model.index(idx.row(), self.COL_R_NAME).data()
1406-
node_addr = model.index(idx.row(), self.COL_R_NODE).data()
1410+
for row in selection:
1411+
node_addr = row[self.COL_R_NODE]
1412+
rule_name = row[self.COL_R_NAME]
14071413

14081414
ok = self._nodes.export_rule(node_addr, rule_name, outdir)
14091415
if not ok:
@@ -1426,9 +1432,9 @@ def _table_menu_export_disk(self, cur_idx, model, selection):
14261432

14271433
def _table_menu_duplicate(self, cur_idx, model, selection):
14281434

1429-
for idx in selection:
1430-
rule_name = model.index(idx.row(), self.COL_R_NAME).data()
1431-
node_addr = model.index(idx.row(), self.COL_R_NODE).data()
1435+
for row in selection:
1436+
node_addr = row[self.COL_R_NODE]
1437+
rule_name = row[self.COL_R_NAME]
14321438

14331439
records = None
14341440
for idx in range(0,100):
@@ -1460,9 +1466,9 @@ def _table_menu_apply_to_node(self, cur_idx, model, selection, node_addr):
14601466

14611467
def _table_menu_change_rule_field(self, cur_idx, model, selection, field, value):
14621468
if cur_idx == self.TAB_RULES and self.rulesTable.isVisible():
1463-
for idx in selection:
1464-
rule_name = model.index(idx.row(), self.COL_R_NAME).data()
1465-
node_addr = model.index(idx.row(), self.COL_R_NODE).data()
1469+
for row in selection:
1470+
rule_name = row[self.COL_R_NAME]
1471+
node_addr = row[self.COL_R_NODE]
14661472

14671473
records = self._get_rule(rule_name, node_addr)
14681474
rule = Rule.new_from_records(records)
@@ -1503,9 +1509,9 @@ def _table_menu_enable(self, cur_idx, model, selection, is_rule_enabled):
15031509
enable_rule = False if is_rule_enabled == "True" else True
15041510

15051511
if cur_idx == self.TAB_RULES and self.rulesTable.isVisible():
1506-
for idx in selection:
1507-
rule_name = model.index(idx.row(), self.COL_R_NAME).data()
1508-
node_addr = model.index(idx.row(), self.COL_R_NODE).data()
1512+
for row in selection:
1513+
rule_name = row[self.COL_R_NAME]
1514+
node_addr = row[self.COL_R_NODE]
15091515

15101516
records = self._get_rule(rule_name, node_addr)
15111517
rule = Rule.new_from_records(records)
@@ -1564,10 +1570,9 @@ def _table_menu_delete(self, cur_idx, model, selection):
15641570
self._notifications_sent[nid] = noti
15651571

15661572
elif cur_idx == self.TAB_RULES and self.rulesTable.isVisible():
1567-
for idx in selection:
1568-
name = model.index(idx.row(), self.COL_R_NAME).data()
1569-
node = model.index(idx.row(), self.COL_R_NODE).data()
1570-
self._del_rule(name, node)
1573+
for row in selection:
1574+
node = row[self.COL_R_NODE]
1575+
name = row[self.COL_R_NAME]
15711576
self._refresh_active_table()
15721577

15731578
elif cur_idx == self.TAB_RULES and self.alertsTable.isVisible():
@@ -1600,9 +1605,9 @@ def _table_menu_new_rule_from_row(self, cur_idx, model, selection):
16001605

16011606
def _table_menu_edit(self, cur_idx, model, selection):
16021607
if cur_idx == self.TAB_RULES and self.rulesTable.isVisible():
1603-
for idx in selection:
1604-
name = model.index(idx.row(), self.COL_R_NAME).data()
1605-
node = model.index(idx.row(), self.COL_R_NODE).data()
1608+
for row in selection:
1609+
node = row[self.COL_R_NODE]
1610+
name = row[self.COL_R_NAME]
16061611
records = self._get_rule(name, node)
16071612
if records == None or records == -1:
16081613
Message.ok(QC.translate("stats", "New rule error"),

0 commit comments

Comments
 (0)