perf scripts python: exported-sql-viewer.py: Add copy to clipboard
authorAdrian Hunter <adrian.hunter@intel.com>
Fri, 3 May 2019 12:08:26 +0000 (15:08 +0300)
committerArnaldo Carvalho de Melo <acme@redhat.com>
Wed, 15 May 2019 19:36:47 +0000 (16:36 -0300)
Add support for copying to clipboard. Two menu options are added to copy the
selected rows / columns with normal spacing, or as comma-separated-values.
In the case of trees, only entire rows can be copied.

Comitter testing:

  $ python ~acme/libexec/perf-core/scripts/python/exported-sql-viewer.py ~/c/adrian.hunter/simple-retpoline.db

Select the lines, press control+C and on the same terminal,
press control+shift+V and voilà:

Call Path                           Object           Count  Time (ns)  Time (%)  Branch Count  Branch Count (%)
  ▼ 14503:14503
    ▼ _start                        ld-2.28.so           1     156267     100.0         10602             100.0
        unknown                     unknown              1       2276       1.5             1               0.0
      ▼ _dl_start                   ld-2.28.so           1     137047      87.7         10088              95.2
        ▶ unknown                   unknown              4       4127       3.0             4               0.0
          _dl_setup_hash            ld-2.28.so           1          0       0.0             1               0.0
        ▶ _dl_sysdep_start          ld-2.28.so           1     131342      95.8          9981              98.9
      ▼ _dl_init                    ld-2.28.so           1       9142       5.9           326               3.1
        ▼ call_init.part.0          ld-2.28.so           3       9133      99.9           319              97.9
          ▶ _init                   libc-2.28.so         1       6877      75.3           110              34.5
          ▶ check_stdfiles_vtables  libc-2.28.so         1         76       0.8             2               0.6
          ▶ init_cacheinfo          libc-2.28.so         1       1991      21.8           197              61.8
      ▶ _start                      simple-retpoline     1       7457       4.8           182               1.7

Signed-off-by: Adrian Hunter <adrian.hunter@intel.com>
Tested-by: Arnaldo Carvalho de Melo <acme@redhat.com>
Cc: Jiri Olsa <jolsa@redhat.com>
Link: http://lkml.kernel.org/r/20190503120828.25326-5-adrian.hunter@intel.com
Signed-off-by: Arnaldo Carvalho de Melo <acme@redhat.com>
tools/perf/scripts/python/exported-sql-viewer.py

index 48953257a1f053f977e4992fd2ae8743a977a2c1..baa2b220238af7a46af0ea6ba4e1d890e525476f 100755 (executable)
@@ -884,6 +884,8 @@ class TreeWindowBase(QMdiSubWindow):
                self.find_bar = None
 
                self.view = QTreeView()
+               self.view.setSelectionMode(QAbstractItemView.ContiguousSelection)
+               self.view.CopyCellsToClipboard = CopyTreeCellsToClipboard
 
        def DisplayFound(self, ids):
                if not len(ids):
@@ -1652,6 +1654,8 @@ class BranchWindow(QMdiSubWindow):
 
                self.view = QTreeView()
                self.view.setUniformRowHeights(True)
+               self.view.setSelectionMode(QAbstractItemView.ContiguousSelection)
+               self.view.CopyCellsToClipboard = CopyTreeCellsToClipboard
                self.view.setModel(self.model)
 
                self.ResizeColumnsToContents()
@@ -2264,6 +2268,207 @@ class ResizeColumnsToContentsBase(QObject):
                self.data_model.rowsInserted.disconnect(self.UpdateColumnWidths)
                self.ResizeColumnsToContents()
 
+# Convert value to CSV
+
+def ToCSValue(val):
+       if '"' in val:
+               val = val.replace('"', '""')
+       if "," in val or '"' in val:
+               val = '"' + val + '"'
+       return val
+
+# Key to sort table model indexes by row / column, assuming fewer than 1000 columns
+
+glb_max_cols = 1000
+
+def RowColumnKey(a):
+       return a.row() * glb_max_cols + a.column()
+
+# Copy selected table cells to clipboard
+
+def CopyTableCellsToClipboard(view, as_csv=False, with_hdr=False):
+       indexes = sorted(view.selectedIndexes(), key=RowColumnKey)
+       idx_cnt = len(indexes)
+       if not idx_cnt:
+               return
+       if idx_cnt == 1:
+               with_hdr=False
+       min_row = indexes[0].row()
+       max_row = indexes[0].row()
+       min_col = indexes[0].column()
+       max_col = indexes[0].column()
+       for i in indexes:
+               min_row = min(min_row, i.row())
+               max_row = max(max_row, i.row())
+               min_col = min(min_col, i.column())
+               max_col = max(max_col, i.column())
+       if max_col > glb_max_cols:
+               raise RuntimeError("glb_max_cols is too low")
+       max_width = [0] * (1 + max_col - min_col)
+       for i in indexes:
+               c = i.column() - min_col
+               max_width[c] = max(max_width[c], len(str(i.data())))
+       text = ""
+       pad = ""
+       sep = ""
+       if with_hdr:
+               model = indexes[0].model()
+               for col in range(min_col, max_col + 1):
+                       val = model.headerData(col, Qt.Horizontal)
+                       if as_csv:
+                               text += sep + ToCSValue(val)
+                               sep = ","
+                       else:
+                               c = col - min_col
+                               max_width[c] = max(max_width[c], len(val))
+                               width = max_width[c]
+                               align = model.headerData(col, Qt.Horizontal, Qt.TextAlignmentRole)
+                               if align & Qt.AlignRight:
+                                       val = val.rjust(width)
+                               text += pad + sep + val
+                               pad = " " * (width - len(val))
+                               sep = "  "
+               text += "\n"
+               pad = ""
+               sep = ""
+       last_row = min_row
+       for i in indexes:
+               if i.row() > last_row:
+                       last_row = i.row()
+                       text += "\n"
+                       pad = ""
+                       sep = ""
+               if as_csv:
+                       text += sep + ToCSValue(str(i.data()))
+                       sep = ","
+               else:
+                       width = max_width[i.column() - min_col]
+                       if i.data(Qt.TextAlignmentRole) & Qt.AlignRight:
+                               val = str(i.data()).rjust(width)
+                       else:
+                               val = str(i.data())
+                       text += pad + sep + val
+                       pad = " " * (width - len(val))
+                       sep = "  "
+       QApplication.clipboard().setText(text)
+
+def CopyTreeCellsToClipboard(view, as_csv=False, with_hdr=False):
+       indexes = view.selectedIndexes()
+       if not len(indexes):
+               return
+
+       selection = view.selectionModel()
+
+       first = None
+       for i in indexes:
+               above = view.indexAbove(i)
+               if not selection.isSelected(above):
+                       first = i
+                       break
+
+       if first is None:
+               raise RuntimeError("CopyTreeCellsToClipboard internal error")
+
+       model = first.model()
+       row_cnt = 0
+       col_cnt = model.columnCount(first)
+       max_width = [0] * col_cnt
+
+       indent_sz = 2
+       indent_str = " " * indent_sz
+
+       expanded_mark_sz = 2
+       if sys.version_info[0] == 3:
+               expanded_mark = "\u25BC "
+               not_expanded_mark = "\u25B6 "
+       else:
+               expanded_mark = unicode(chr(0xE2) + chr(0x96) + chr(0xBC) + " ", "utf-8")
+               not_expanded_mark =  unicode(chr(0xE2) + chr(0x96) + chr(0xB6) + " ", "utf-8")
+       leaf_mark = "  "
+
+       if not as_csv:
+               pos = first
+               while True:
+                       row_cnt += 1
+                       row = pos.row()
+                       for c in range(col_cnt):
+                               i = pos.sibling(row, c)
+                               if c:
+                                       n = len(str(i.data()))
+                               else:
+                                       n = len(str(i.data()).strip())
+                                       n += (i.internalPointer().level - 1) * indent_sz
+                                       n += expanded_mark_sz
+                               max_width[c] = max(max_width[c], n)
+                       pos = view.indexBelow(pos)
+                       if not selection.isSelected(pos):
+                               break
+
+       text = ""
+       pad = ""
+       sep = ""
+       if with_hdr:
+               for c in range(col_cnt):
+                       val = model.headerData(c, Qt.Horizontal, Qt.DisplayRole).strip()
+                       if as_csv:
+                               text += sep + ToCSValue(val)
+                               sep = ","
+                       else:
+                               max_width[c] = max(max_width[c], len(val))
+                               width = max_width[c]
+                               align = model.headerData(c, Qt.Horizontal, Qt.TextAlignmentRole)
+                               if align & Qt.AlignRight:
+                                       val = val.rjust(width)
+                               text += pad + sep + val
+                               pad = " " * (width - len(val))
+                               sep = "   "
+               text += "\n"
+               pad = ""
+               sep = ""
+
+       pos = first
+       while True:
+               row = pos.row()
+               for c in range(col_cnt):
+                       i = pos.sibling(row, c)
+                       val = str(i.data())
+                       if not c:
+                               if model.hasChildren(i):
+                                       if view.isExpanded(i):
+                                               mark = expanded_mark
+                                       else:
+                                               mark = not_expanded_mark
+                               else:
+                                       mark = leaf_mark
+                               val = indent_str * (i.internalPointer().level - 1) + mark + val.strip()
+                       if as_csv:
+                               text += sep + ToCSValue(val)
+                               sep = ","
+                       else:
+                               width = max_width[c]
+                               if c and i.data(Qt.TextAlignmentRole) & Qt.AlignRight:
+                                       val = val.rjust(width)
+                               text += pad + sep + val
+                               pad = " " * (width - len(val))
+                               sep = "   "
+               pos = view.indexBelow(pos)
+               if not selection.isSelected(pos):
+                       break
+               text = text.rstrip() + "\n"
+               pad = ""
+               sep = ""
+
+       QApplication.clipboard().setText(text)
+
+def CopyCellsToClipboard(view, as_csv=False, with_hdr=False):
+       view.CopyCellsToClipboard(view, as_csv, with_hdr)
+
+def CopyCellsToClipboardHdr(view):
+       CopyCellsToClipboard(view, False, True)
+
+def CopyCellsToClipboardCSV(view):
+       CopyCellsToClipboard(view, True, True)
+
 # Table window
 
 class TableWindow(QMdiSubWindow, ResizeColumnsToContentsBase):
@@ -2282,6 +2487,8 @@ class TableWindow(QMdiSubWindow, ResizeColumnsToContentsBase):
                self.view.verticalHeader().setVisible(False)
                self.view.sortByColumn(-1, Qt.AscendingOrder)
                self.view.setSortingEnabled(True)
+               self.view.setSelectionMode(QAbstractItemView.ContiguousSelection)
+               self.view.CopyCellsToClipboard = CopyTableCellsToClipboard
 
                self.ResizeColumnsToContents()
 
@@ -2398,6 +2605,8 @@ class TopCallsWindow(QMdiSubWindow, ResizeColumnsToContentsBase):
                self.view.setModel(self.model)
                self.view.setEditTriggers(QAbstractItemView.NoEditTriggers)
                self.view.verticalHeader().setVisible(False)
+               self.view.setSelectionMode(QAbstractItemView.ContiguousSelection)
+               self.view.CopyCellsToClipboard = CopyTableCellsToClipboard
 
                self.ResizeColumnsToContents()
 
@@ -2735,6 +2944,8 @@ class MainWindow(QMainWindow):
                file_menu.addAction(CreateExitAction(glb.app, self))
 
                edit_menu = menu.addMenu("&Edit")
+               edit_menu.addAction(CreateAction("&Copy", "Copy to clipboard", self.CopyToClipboard, self, QKeySequence.Copy))
+               edit_menu.addAction(CreateAction("Copy as CS&V", "Copy to clipboard as CSV", self.CopyToClipboardCSV, self))
                edit_menu.addAction(CreateAction("&Find...", "Find items", self.Find, self, QKeySequence.Find))
                edit_menu.addAction(CreateAction("Fetch &more records...", "Fetch more records", self.FetchMoreRecords, self, [QKeySequence(Qt.Key_F8)]))
                edit_menu.addAction(CreateAction("&Shrink Font", "Make text smaller", self.ShrinkFont, self, [QKeySequence("Ctrl+-")]))
@@ -2767,6 +2978,12 @@ class MainWindow(QMainWindow):
                        except:
                                pass
 
+       def CopyToClipboard(self):
+               self.Try(CopyCellsToClipboardHdr)
+
+       def CopyToClipboardCSV(self):
+               self.Try(CopyCellsToClipboardCSV)
+
        def Find(self):
                win = self.mdi_area.activeSubWindow()
                if win: