Add copy buttons for protolog source files.

buttons appear on hover

Bug: 406270438
Test: npm run test:unit:ci
Change-Id: I76a66a192eee4f8dedfd09a679438aaecd62a2e5
diff --git a/tools/winscope/src/viewers/common/ui_data_log.ts b/tools/winscope/src/viewers/common/ui_data_log.ts
index a41841e..d22a8bb 100644
--- a/tools/winscope/src/viewers/common/ui_data_log.ts
+++ b/tools/winscope/src/viewers/common/ui_data_log.ts
@@ -39,6 +39,7 @@
 export interface ColumnSpec {
   name: string;
   cssClass: string;
+  canCopy?: boolean;
 }
 
 export class LogHeader {
diff --git a/tools/winscope/src/viewers/components/log_component.ts b/tools/winscope/src/viewers/components/log_component.ts
index 1f73e43..0acfbca 100644
--- a/tools/winscope/src/viewers/components/log_component.ts
+++ b/tools/winscope/src/viewers/components/log_component.ts
@@ -187,7 +187,7 @@
             </button>
           </div>
 
-          <div [class]="field.spec.cssClass" *ngFor="let field of entry.fields; index as i">
+          <div [class]="field.spec.cssClass + ' cell'" *ngFor="let field of entry.fields; index as i">
             <span class="mat-body-1" *ngIf="!showFieldButton(entry, field)">{{ field.value }}</span>
             <button
                 *ngIf="showFieldButton(entry, field)"
@@ -201,6 +201,13 @@
                 *ngIf="field.icon"
                 aria-hidden="false"
                 [style]="{color: field.iconColor}"> {{field.icon}} </mat-icon>
+            <button
+                mat-icon-button
+                *ngIf="field.spec.canCopy"
+                class="copy-button"
+                [cdkCopyToClipboard]="field.value.toString()">
+              <mat-icon>content_copy</mat-icon>
+            </button>
           </div>
         </div>
       </ng-template>
diff --git a/tools/winscope/src/viewers/components/log_component_test.ts b/tools/winscope/src/viewers/components/log_component_test.ts
index 40c7b9b..79a6d70 100644
--- a/tools/winscope/src/viewers/components/log_component_test.ts
+++ b/tools/winscope/src/viewers/components/log_component_test.ts
@@ -14,6 +14,7 @@
  * limitations under the License.
  */
 
+import {Clipboard, ClipboardModule} from '@angular/cdk/clipboard';
 import {ScrollingModule} from '@angular/cdk/scrolling';
 import {ComponentFixtureAutoDetect, TestBed} from '@angular/core/testing';
 import {FormsModule} from '@angular/forms';
@@ -63,10 +64,15 @@
 
   let component: LogComponent;
   let dom: DOMTestHelper<LogComponent>;
+  let mockCopyText: jasmine.Spy;
 
   beforeEach(async () => {
+    mockCopyText = jasmine.createSpy();
     await TestBed.configureTestingModule({
-      providers: [{provide: ComponentFixtureAutoDetect, useValue: true}],
+      providers: [
+        {provide: Clipboard, useValue: {copy: mockCopyText}},
+        {provide: ComponentFixtureAutoDetect, useValue: true},
+      ],
       imports: [
         ScrollingModule,
         MatFormFieldModule,
@@ -80,6 +86,7 @@
         MatPseudoCheckboxModule,
         MatProgressSpinnerModule,
         MatTooltipModule,
+        ClipboardModule,
       ],
       declarations: [
         LogComponent,
@@ -297,6 +304,19 @@
     entry.checkTextExact('00:00:00.000 Test tag 21234 N/A');
   });
 
+  it('shows copy button for spec that can be copied', () => {
+    const entry = dom.get('.scroll .entry .test-2');
+    expect(entry.find('.copy-button')).toBeUndefined();
+    component.entries[0].fields[1].spec = {
+      name: 'test2',
+      cssClass: 'test-2',
+      canCopy: true,
+    };
+    dom.detectChanges();
+    entry.findAndClick('.copy-button');
+    expect(mockCopyText).toHaveBeenCalledOnceWith('123');
+  });
+
   function setComponentInputData(elapsed = true) {
     let entryTime: Timestamp;
     let fieldTime: Timestamp;
diff --git a/tools/winscope/src/viewers/components/styles/log_component.styles.ts b/tools/winscope/src/viewers/components/styles/log_component.styles.ts
index 8c4bd4f..916b916 100644
--- a/tools/winscope/src/viewers/components/styles/log_component.styles.ts
+++ b/tools/winscope/src/viewers/components/styles/log_component.styles.ts
@@ -34,10 +34,6 @@
     display: flex;
     flex-direction: row;
     overflow-wrap: anywhere;
-  }
-
-  .headers div,
-  .entries div {
     padding: 4px;
   }
 
@@ -45,6 +41,10 @@
     align-content: center;
   }
 
+  .header, .filter, .cell {
+    padding: 4px;
+  }
+
   .time {
     flex: 1;
     min-width: 135px;
@@ -179,10 +179,25 @@
     justify-content: end;
   }
 
-  .status .mat-icon {
+  .entry .source-file {
+    display: flex;
+    align-items: start;
+    justify-content: space-between;
+  }
+
+  .status .mat-icon, .copy-button, .copy-button .mat-icon {
     font-size: 18px;
     width: 18px;
     height: 18px;
+    line-height: 18px;
+  }
+
+  .cell:not(:hover) .copy-button {
+    visibility: hidden;
+  }
+
+  .copy-button .mat-icon {
+    min-width: 18px;
   }
 
   .input-type {
diff --git a/tools/winscope/src/viewers/viewer_input/presenter.ts b/tools/winscope/src/viewers/viewer_input/presenter.ts
index bf8b4a8..e7ffb3e 100644
--- a/tools/winscope/src/viewers/viewer_input/presenter.ts
+++ b/tools/winscope/src/viewers/viewer_input/presenter.ts
@@ -367,10 +367,10 @@
   ): string {
     const keyDetails =
       'Keycode: ' +
-        eventTree
-          .getChildByName('keyCode')
-          ?.formattedValue()
-          ?.replace(/^KEYCODE_/, '') ?? '<?>';
+      (eventTree
+        .getChildByName('keyCode')
+        ?.formattedValue()
+        ?.replace(/^KEYCODE_/, '') ?? '<?>');
     return keyDetails + ' ' + Presenter.extractDispatchDetails(dispatchTree);
   }
 
diff --git a/tools/winscope/src/viewers/viewer_protolog/presenter.ts b/tools/winscope/src/viewers/viewer_protolog/presenter.ts
index dfbcb1c..ba4d957 100644
--- a/tools/winscope/src/viewers/viewer_protolog/presenter.ts
+++ b/tools/winscope/src/viewers/viewer_protolog/presenter.ts
@@ -44,6 +44,7 @@
     sourceFile: {
       name: 'Source files',
       cssClass: 'source-file',
+      canCopy: true,
     },
     text: {
       name: 'Search text',
diff --git a/tools/winscope/src/viewers/viewer_protolog/presenter_test.ts b/tools/winscope/src/viewers/viewer_protolog/presenter_test.ts
index b33b45f..17cdda2 100644
--- a/tools/winscope/src/viewers/viewer_protolog/presenter_test.ts
+++ b/tools/winscope/src/viewers/viewer_protolog/presenter_test.ts
@@ -54,7 +54,7 @@
     },
     {
       header: new LogHeader(
-        {name: 'Source files', cssClass: 'source-file'},
+        {name: 'Source files', cssClass: 'source-file', canCopy: true},
         new LogSelectFilter(Array.from({length: 3}, () => '')),
       ),
       options: ['sourcefile0', 'sourcefile1', 'sourcefile2'],