blob: f460081f1712440cec97f9a8d78dfcc39d2a128d [file] [log] [blame]
/*
* Copyright (C) 2019 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.tools.idea.layoutinspector.ui
import com.android.tools.adtui.ZOOMABLE_KEY
import com.android.tools.adtui.Zoomable
import com.android.tools.adtui.actions.DropDownAction
import com.android.tools.adtui.actions.ZoomInAction
import com.android.tools.adtui.actions.ZoomLabelAction
import com.android.tools.adtui.actions.ZoomOutAction
import com.android.tools.adtui.actions.ZoomToFitAction
import com.android.tools.adtui.actions.ZoomType
import com.android.tools.adtui.common.AdtPrimaryPanel
import com.android.tools.idea.layoutinspector.LayoutInspector
import com.android.tools.idea.layoutinspector.SkiaParser
import com.android.tools.idea.layoutinspector.model.InspectorModel
import com.android.tools.idea.layoutinspector.transport.InspectorClient
import com.android.tools.layoutinspector.proto.LayoutInspectorProto.LayoutInspectorEvent
import com.android.tools.profiler.proto.Common
import com.intellij.icons.AllIcons
import com.intellij.openapi.actionSystem.ActionManager
import com.intellij.openapi.actionSystem.AnAction
import com.intellij.openapi.actionSystem.AnActionEvent
import com.intellij.openapi.actionSystem.DataProvider
import com.intellij.openapi.actionSystem.DefaultActionGroup
import com.intellij.openapi.actionSystem.ex.CheckboxAction
import com.intellij.openapi.application.ApplicationManager
import com.intellij.openapi.util.text.StringUtil
import com.intellij.ui.components.JBScrollPane
import icons.StudioIcons
import java.awt.BorderLayout
import javax.swing.BorderFactory
import javax.swing.Icon
import javax.swing.JComponent
import javax.swing.JPanel
/**
* Panel that shows the device screen in the layout inspector.
*/
class DeviceViewPanel(private val layoutInspector: LayoutInspector) : JPanel(BorderLayout()), Zoomable, DataProvider {
private val client = layoutInspector.client
enum class ViewMode(val icon: Icon) {
FIXED(StudioIcons.LayoutEditor.Extras.ROOT_INLINE),
X_ONLY(StudioIcons.DeviceConfiguration.SCREEN_WIDTH),
XY(StudioIcons.DeviceConfiguration.SMALLEST_SCREEN_SIZE);
val next: ViewMode
get() = enumValues<ViewMode>()[(this.ordinal + 1).rem(enumValues<ViewMode>().size)]
}
var viewMode = ViewMode.XY
override var scale: Double = .5
override val screenScalingFactor = 1f
private var drawBorders = true
private val showBordersCheckBox = object : CheckboxAction("Show borders") {
override fun isSelected(e: AnActionEvent): Boolean {
return drawBorders
}
override fun setSelected(e: AnActionEvent, state: Boolean) {
drawBorders = state
repaint()
}
}
private val myProcessSelectionAction = SelectProcessAction(client)
val contentPanel = DeviceViewContentPanel(layoutInspector, scale, viewMode)
private val scrollPane = JBScrollPane(contentPanel)
init {
client.register(Common.Event.EventGroupIds.SKIA_PICTURE) { handleSkiaPictureEvent(it) }
layoutInspector.modelChangeListeners.add(::modelChanged)
add(createToolbar(), BorderLayout.NORTH)
add(scrollPane, BorderLayout.CENTER)
}
override fun zoom(type: ZoomType): Boolean {
val position = scrollPane.viewport.viewPosition.apply { translate(scrollPane.viewport.width / 2, scrollPane.viewport.height / 2) }
position.x = (position.x / scale).toInt()
position.y = (position.y / scale).toInt()
when (type) {
ZoomType.FIT, ZoomType.FIT_INTO, ZoomType.SCREEN -> scale = 0.5
ZoomType.ACTUAL -> scale = 1.0
ZoomType.IN -> scale += 0.1
ZoomType.OUT -> scale -= 0.1
}
contentPanel.scale = scale
scrollPane.viewport.revalidate()
position.x = (position.x * scale).toInt()
position.y = (position.y * scale).toInt()
position.translate(-scrollPane.viewport.width / 2, -scrollPane.viewport.height / 2)
scrollPane.viewport.viewPosition = position
return true
}
override fun canZoomIn() = true
override fun canZoomOut() = true
override fun canZoomToFit() = true
override fun getData(dataId: String): Any? {
if (ZOOMABLE_KEY.`is`(dataId)) {
return this
}
return null
}
private fun createToolbar(): JComponent {
val panel = AdtPrimaryPanel(BorderLayout())
panel.border = BorderFactory.createMatteBorder(0, 0, 1, 0, com.android.tools.adtui.common.border)!!
val leftPanel = AdtPrimaryPanel(BorderLayout())
val leftGroup = DefaultActionGroup()
leftGroup.add(myProcessSelectionAction)
leftGroup.add(showBordersCheckBox)
leftPanel.add(ActionManager.getInstance().createActionToolbar("DynamicLayoutInspectorLeft", leftGroup, true).component,
BorderLayout.CENTER)
panel.add(leftPanel, BorderLayout.CENTER)
val rightGroup = DefaultActionGroup()
rightGroup.add(object : AnAction("reset") {
override fun actionPerformed(e: AnActionEvent) {
viewMode = viewMode.next
contentPanel.viewMode = viewMode
}
override fun update(e: AnActionEvent) {
e.presentation.icon = viewMode.icon
}
})
rightGroup.add(ZoomOutAction)
rightGroup.add(ZoomLabelAction)
rightGroup.add(ZoomInAction)
rightGroup.add(ZoomToFitAction)
val toolbar = ActionManager.getInstance().createActionToolbar("DynamicLayoutInspectorRight", rightGroup, true)
toolbar.setTargetComponent(this)
panel.add(toolbar.component, BorderLayout.EAST)
return panel
}
@Suppress("UNUSED_PARAMETER")
private fun modelChanged(old: InspectorModel, new: InspectorModel) {
scrollPane.viewport.revalidate()
repaint()
}
private fun handleSkiaPictureEvent(event: LayoutInspectorEvent) {
val application = ApplicationManager.getApplication()
application.executeOnPooledThread {
val bytes = client.getPayload(event.payloadId)
if (bytes.isNotEmpty()) {
SkiaParser().getViewTree(bytes)?.let {
layoutInspector.layoutInspectorModel.update(it)
application.invokeLater {
scrollPane.viewport.revalidate()
repaint()
}
}
}
}
}
// TODO: Replace this with the process selector from the profiler
private class SelectProcessAction(val client: InspectorClient) :
DropDownAction("Select Process", "Select a process to connect to.", AllIcons.General.Add) {
override fun updateActions(): Boolean {
removeAll()
// Rebuild the action tree.
val processesMap = client.loadProcesses()
if (processesMap.isEmpty()) {
val noDeviceAction = object : AnAction("No devices detected") {
override fun actionPerformed(e: AnActionEvent) {}
}
noDeviceAction.templatePresentation.isEnabled = false
add(noDeviceAction)
}
else {
for (stream in processesMap.keys) {
val deviceAction = DropDownAction(buildDeviceName(stream.device), null, null)
val processes = processesMap[stream]
if (processes == null || processes.isEmpty()) {
val noProcessAction = object : AnAction("No debuggable processes detected") {
override fun actionPerformed(e: AnActionEvent) {}
}
noProcessAction.templatePresentation.isEnabled = false
deviceAction.add(noProcessAction)
}
else {
for (process in processes) {
val processAction = object : AnAction("${process.name} (${process.pid})") {
override fun actionPerformed(event: AnActionEvent) {
client.attach(stream, process)
}
}
deviceAction.add(processAction)
}
}
add(deviceAction)
}
}
return true
}
private fun buildDeviceName(device: Common.Device): String {
val deviceNameBuilder = StringBuilder()
val manufacturer = device.manufacturer
var model = device.model
val serial = device.serial
val suffix = String.format("-%s", serial)
if (model.endsWith(suffix)) {
model = model.substring(0, model.length - suffix.length)
}
if (!StringUtil.isEmpty(manufacturer)) {
deviceNameBuilder.append(manufacturer)
deviceNameBuilder.append(" ")
}
deviceNameBuilder.append(model)
return deviceNameBuilder.toString()
}
}
}