From 4bb1a411e8573f0ea0223aa799df8bf18e1144f1 Mon Sep 17 00:00:00 2001 From: hstyi Date: Sat, 15 Mar 2025 13:15:55 +0800 Subject: [PATCH] feat: without jbr --- build.gradle.kts | 17 +- .../kotlin/app/termora/ApplicationRunner.kt | 15 +- src/main/kotlin/app/termora/DialogWrapper.kt | 125 ++++++--- src/main/kotlin/app/termora/InputDialog.kt | 74 ------ .../kotlin/app/termora/LogicCustomTitleBar.kt | 109 -------- .../kotlin/app/termora/MyFlatRootPaneUI.kt | 11 + src/main/kotlin/app/termora/OptionPane.kt | 65 ++++- src/main/kotlin/app/termora/TerminalTabbed.kt | 5 +- src/main/kotlin/app/termora/TermoraFrame.kt | 251 +++++++++++------- src/main/kotlin/app/termora/TermoraToolBar.kt | 19 +- .../termora/findeverywhere/FindEverywhere.kt | 19 +- .../kotlin/app/termora/keymap/KeymapPanel.kt | 7 +- .../app/termora/keymgr/KeyManagerPanel.kt | 19 +- .../kotlin/app/termora/macro/MacroDialog.kt | 7 +- .../termora/native/osx/NativeMacLibrary.kt | 51 ++++ .../app/termora/sftp/FileSystemViewTable.kt | 16 +- 16 files changed, 405 insertions(+), 405 deletions(-) delete mode 100644 src/main/kotlin/app/termora/InputDialog.kt delete mode 100644 src/main/kotlin/app/termora/LogicCustomTitleBar.kt create mode 100644 src/main/kotlin/app/termora/MyFlatRootPaneUI.kt create mode 100644 src/main/kotlin/app/termora/native/osx/NativeMacLibrary.kt diff --git a/build.gradle.kts b/build.gradle.kts index a966e65..60118c1 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -118,7 +118,6 @@ dependencies { application { val args = mutableListOf( - "--add-exports java.base/sun.nio.ch=ALL-UNNAMED", "-Xmx2g", "-XX:+UseZGC", "-XX:+ZUncommit", @@ -127,6 +126,10 @@ application { ) if (os.isMacOsX) { + // macOS NSWindow + args.add("--add-opens java.desktop/java.awt=ALL-UNNAMED") + args.add("--add-opens java.desktop/sun.lwawt=ALL-UNNAMED") + args.add("--add-opens java.desktop/sun.lwawt.macosx=ALL-UNNAMED") args.add("-Dsun.java2d.metal=true") args.add("-Dapple.awt.application.appearance=system") } @@ -344,6 +347,10 @@ tasks.register("jpackage") { options.add("-Dsun.java2d.metal=true") if (os.isMacOsX) { + // NSWindow + options.add("--add-opens java.desktop/java.awt=ALL-UNNAMED") + options.add("--add-opens java.desktop/sun.lwawt=ALL-UNNAMED") + options.add("--add-opens java.desktop/sun.lwawt.macosx=ALL-UNNAMED") options.add("-Dapple.awt.application.appearance=system") options.add("--add-opens java.desktop/sun.lwawt.macosx.concurrent=ALL-UNNAMED") } @@ -420,15 +427,9 @@ tasks.register("jpackage") { tasks.register("dist") { doLast { - val vendor = Jvm.current().vendor ?: StringUtils.EMPTY - @Suppress("UnstableApiUsage") - if (!JvmVendorSpec.JETBRAINS.matches(vendor)) { - throw GradleException("JVM: $vendor is not supported") - } val gradlew = File(projectDir, if (os.isWindows) "gradlew.bat" else "gradlew").absolutePath - // 清空目录 exec { commandLine(gradlew, "clean") } @@ -735,8 +736,6 @@ fun stapleMacOSLocalFile(file: File) { kotlin { jvmToolchain { languageVersion = JavaLanguageVersion.of(21) - @Suppress("UnstableApiUsage") - vendor = JvmVendorSpec.JETBRAINS } } diff --git a/src/main/kotlin/app/termora/ApplicationRunner.kt b/src/main/kotlin/app/termora/ApplicationRunner.kt index 64555c5..1c472cf 100644 --- a/src/main/kotlin/app/termora/ApplicationRunner.kt +++ b/src/main/kotlin/app/termora/ApplicationRunner.kt @@ -144,7 +144,7 @@ class ApplicationRunner { private fun setupLaf() { - System.setProperty(FlatSystemProperties.USE_WINDOW_DECORATIONS, "${SystemInfo.isLinux}") + System.setProperty(FlatSystemProperties.USE_WINDOW_DECORATIONS, "${SystemInfo.isLinux || SystemInfo.isWindows}") System.setProperty(FlatSystemProperties.USE_ROUNDED_POPUP_BORDER, "false") if (SystemInfo.isLinux) { @@ -152,15 +152,6 @@ class ApplicationRunner { JDialog.setDefaultLookAndFeelDecorated(true) } - UIManager.put( - "FileChooser.${if (SystemInfo.isWindows) "win32" else "other"}.newFolder", - I18n.getString("termora.welcome.contextmenu.new.folder.name") - ) - UIManager.put( - "FileChooser.${if (SystemInfo.isWindows) "win32" else "other"}.newFolder.subsequent", - "${I18n.getString("termora.welcome.contextmenu.new.folder.name")}.{0}" - ) - val themeManager = ThemeManager.getInstance() val appearance = Database.getDatabase().appearance var theme = appearance.theme @@ -176,8 +167,8 @@ class ApplicationRunner { themeManager.change(theme, true) - if (Application.isUnknownVersion()) - FlatInspector.install("ctrl shift alt X") +// if (Application.isUnknownVersion()) + FlatInspector.install("ctrl X") UIManager.put(FlatClientProperties.FULL_WINDOW_CONTENT, true) UIManager.put(FlatClientProperties.USE_WINDOW_DECORATIONS, false) diff --git a/src/main/kotlin/app/termora/DialogWrapper.kt b/src/main/kotlin/app/termora/DialogWrapper.kt index f1b1520..b54dffd 100644 --- a/src/main/kotlin/app/termora/DialogWrapper.kt +++ b/src/main/kotlin/app/termora/DialogWrapper.kt @@ -2,8 +2,8 @@ package app.termora import app.termora.actions.AnAction import app.termora.actions.AnActionEvent +import app.termora.native.osx.NativeMacLibrary import com.formdev.flatlaf.FlatClientProperties -import com.formdev.flatlaf.FlatLaf import com.formdev.flatlaf.util.SystemInfo import com.jetbrains.JBR import java.awt.* @@ -12,29 +12,60 @@ import java.awt.event.WindowAdapter import java.awt.event.WindowEvent import javax.swing.* - abstract class DialogWrapper(owner: Window?) : JDialog(owner) { - private val rootPanel = JPanel(BorderLayout()) private val titleLabel = JLabel() - private val titleBar by lazy { LogicCustomTitleBar.createCustomTitleBar(this) } val disposable = Disposer.newDisposable() + private val customTitleBar = if (SystemInfo.isMacOS && JBR.isWindowDecorationsSupported()) + JBR.getWindowDecorations().createCustomTitleBar() else null companion object { const val DEFAULT_ACTION = "DEFAULT_ACTION" private const val PROCESS_GLOBAL_KEYMAP = "PROCESS_GLOBAL_KEYMAP" } - protected var controlsVisible = true set(value) { field = value - titleBar.putProperty("controls.visible", value) + if (SystemInfo.isMacOS) { + if (customTitleBar != null) { + customTitleBar.putProperty("controls.visible", value) + } else { + NativeMacLibrary.setControlsVisible(this, value) + } + } else { + rootPane.putClientProperty(FlatClientProperties.TITLE_BAR_SHOW_ICONIFFY, value) + rootPane.putClientProperty(FlatClientProperties.TITLE_BAR_SHOW_MAXIMIZE, value) + rootPane.putClientProperty(FlatClientProperties.TITLE_BAR_SHOW_CLOSE, value) + } } - protected var titleBarHeight = UIManager.getInt("TabbedPane.tabHeight").toFloat() + protected var fullWindowContent = false set(value) { - titleBar.height = value field = value + rootPane.putClientProperty(FlatClientProperties.FULL_WINDOW_CONTENT, value) + } + + protected var titleVisible = true + set(value) { + field = value + rootPane.putClientProperty(FlatClientProperties.TITLE_BAR_SHOW_TITLE, value) + } + + protected var titleIconVisible = false + set(value) { + field = value + rootPane.putClientProperty(FlatClientProperties.TITLE_BAR_SHOW_ICON, value) + } + + + protected var titleBarHeight = UIManager.getInt("TabbedPane.tabHeight") + set(value) { + field = value + if (SystemInfo.isMacOS) { + customTitleBar?.height = height.toFloat() + } else { + rootPane.putClientProperty(FlatClientProperties.TITLE_BAR_HEIGHT, value) + } } protected var lostFocusDispose = false @@ -51,25 +82,43 @@ abstract class DialogWrapper(owner: Window?) : JDialog(owner) { super.rootPane.putClientProperty(PROCESS_GLOBAL_KEYMAP, value) } + init { + super.setDefaultCloseOperation(DISPOSE_ON_CLOSE) + + // 使用 FlatLaf 的 TitlePane + if (SystemInfo.isWindows || SystemInfo.isLinux) { + rootPane.windowDecorationStyle = JRootPane.PLAIN_DIALOG + } + } + protected fun init() { - - - defaultCloseOperation = DISPOSE_ON_CLOSE - - initTitleBar() initEvents() - if (JBR.isWindowDecorationsSupported()) { - if (rootPane.getClientProperty(FlatClientProperties.TITLE_BAR_SHOW_TITLE) != false) { - val titlePanel = createTitlePanel() - if (titlePanel != null) { - rootPanel.add(titlePanel, BorderLayout.NORTH) - } + val rootPanel = JPanel(BorderLayout()) + rootPanel.add(createCenterPanel(), BorderLayout.CENTER) + + if (SystemInfo.isMacOS) { + rootPane.putClientProperty("apple.awt.windowTitleVisible", false) + rootPane.putClientProperty("apple.awt.fullWindowContent", true) + rootPane.putClientProperty("apple.awt.transparentTitleBar", true) + rootPane.putClientProperty( + FlatClientProperties.MACOS_WINDOW_BUTTONS_SPACING, + FlatClientProperties.MACOS_WINDOW_BUTTONS_SPACING_MEDIUM + ) + + val titlePanel = createTitlePanel() + if (titlePanel != null) { + rootPanel.add(titlePanel, BorderLayout.NORTH) + } + + val customTitleBar = this.customTitleBar + if (customTitleBar != null) { + customTitleBar.putProperty("controls.visible", controlsVisible) + customTitleBar.height = titleBarHeight.toFloat() + JBR.getWindowDecorations().setCustomTitleBar(this, customTitleBar) } } - rootPanel.add(createCenterPanel(), BorderLayout.CENTER) - val southPanel = createSouthPanel() if (southPanel != null) { rootPanel.add(southPanel, BorderLayout.SOUTH) @@ -122,7 +171,7 @@ abstract class DialogWrapper(owner: Window?) : JDialog(owner) { val panel = JPanel(BorderLayout()) panel.add(titleLabel, BorderLayout.CENTER) - panel.preferredSize = Dimension(-1, titleBar.height.toInt()) + panel.preferredSize = Dimension(-1, titleBarHeight) return panel @@ -191,30 +240,20 @@ abstract class DialogWrapper(owner: Window?) : JDialog(owner) { } }) - if (SystemInfo.isWindows) { - addWindowListener(object : WindowAdapter(), ThemeChangeListener { - override fun windowClosed(e: WindowEvent) { - ThemeManager.getInstance().removeThemeChangeListener(this) - } - - override fun windowOpened(e: WindowEvent) { - onChanged() - ThemeManager.getInstance().addThemeChangeListener(this) - } - - override fun onChanged() { - titleBar.putProperty("controls.dark", FlatLaf.isLafDark()) - } - }) - } } - private fun initTitleBar() { - titleBar.height = titleBarHeight - titleBar.putProperty("controls.visible", controlsVisible) - if (JBR.isWindowDecorationsSupported()) { - JBR.getWindowDecorations().setCustomTitleBar(this, titleBar) + override fun addNotify() { + super.addNotify() + + // 显示后触发一次重绘制 + if (SystemInfo.isWindows || SystemInfo.isLinux) { + this.controlsVisible = controlsVisible + this.titleBarHeight = titleBarHeight + this.titleIconVisible = titleIconVisible + this.titleVisible = titleVisible + this.fullWindowContent = fullWindowContent } + } protected open fun doOKAction() { diff --git a/src/main/kotlin/app/termora/InputDialog.kt b/src/main/kotlin/app/termora/InputDialog.kt deleted file mode 100644 index 56f7503..0000000 --- a/src/main/kotlin/app/termora/InputDialog.kt +++ /dev/null @@ -1,74 +0,0 @@ -package app.termora - -import com.formdev.flatlaf.extras.components.FlatTextField -import org.apache.commons.lang3.StringUtils -import java.awt.Window -import java.awt.event.KeyAdapter -import java.awt.event.KeyEvent -import javax.swing.BorderFactory -import javax.swing.JComponent -import javax.swing.UIManager - -class InputDialog( - owner: Window, - title: String, - text: String = StringUtils.EMPTY, - placeholderText: String = StringUtils.EMPTY -) : DialogWrapper(owner) { - private val textField = FlatTextField() - private var text: String? = null - - init { - setSize(340, 60) - setLocationRelativeTo(owner) - - super.setTitle(title) - - isResizable = false - isModal = true - controlsVisible = false - titleBarHeight = UIManager.getInt("TabbedPane.tabHeight") * 0.8f - - - textField.placeholderText = placeholderText - textField.text = text - textField.addKeyListener(object : KeyAdapter() { - override fun keyPressed(e: KeyEvent) { - if (e.keyCode == KeyEvent.VK_ENTER) { - if (textField.text.isBlank()) { - return - } - doOKAction() - } - } - }) - - init() - } - - override fun createCenterPanel(): JComponent { - textField.background = UIManager.getColor("window") - textField.border = BorderFactory.createEmptyBorder(0, 13, 0, 13) - - return textField - } - - fun getText(): String? { - isVisible = true - return text - } - - override fun doCancelAction() { - text = null - super.doCancelAction() - } - - override fun doOKAction() { - text = textField.text - super.doOKAction() - } - - override fun createSouthPanel(): JComponent? { - return null - } -} \ No newline at end of file diff --git a/src/main/kotlin/app/termora/LogicCustomTitleBar.kt b/src/main/kotlin/app/termora/LogicCustomTitleBar.kt deleted file mode 100644 index fa38c65..0000000 --- a/src/main/kotlin/app/termora/LogicCustomTitleBar.kt +++ /dev/null @@ -1,109 +0,0 @@ -package app.termora - -import com.formdev.flatlaf.FlatClientProperties -import com.jetbrains.JBR -import com.jetbrains.WindowDecorations.CustomTitleBar -import java.awt.Rectangle -import java.awt.Window -import javax.swing.RootPaneContainer - -class LogicCustomTitleBar(private val titleBar: CustomTitleBar) : CustomTitleBar { - companion object { - fun createCustomTitleBar(rootPaneContainer: RootPaneContainer): CustomTitleBar { - if (!JBR.isWindowDecorationsSupported()) { - return LogicCustomTitleBar(object : CustomTitleBar { - override fun getHeight(): Float { - val bounds = rootPaneContainer.rootPane - .getClientProperty(FlatClientProperties.FULL_WINDOW_CONTENT_BUTTONS_BOUNDS) - if (bounds is Rectangle) { - return bounds.height.toFloat() - } - return 0f - } - - override fun setHeight(height: Float) { - rootPaneContainer.rootPane.putClientProperty( - FlatClientProperties.TITLE_BAR_HEIGHT, - height.toInt() - ) - } - - override fun getProperties(): MutableMap { - return mutableMapOf() - } - - override fun putProperties(m: MutableMap?) { - - } - - override fun putProperty(key: String?, value: Any?) { - if (key == "controls.visible" && value is Boolean) { - rootPaneContainer.rootPane.putClientProperty( - FlatClientProperties.TITLE_BAR_SHOW_CLOSE, - value - ) - } - } - - override fun getLeftInset(): Float { - return 0f - } - - override fun getRightInset(): Float { - val bounds = rootPaneContainer.rootPane - .getClientProperty(FlatClientProperties.FULL_WINDOW_CONTENT_BUTTONS_BOUNDS) - if (bounds is Rectangle) { - return bounds.width.toFloat() - } - return 0f - } - - override fun forceHitTest(client: Boolean) { - - } - - override fun getContainingWindow(): Window { - return rootPaneContainer as Window - } - }) - } - return JBR.getWindowDecorations().createCustomTitleBar() - } - } - - override fun getHeight(): Float { - return titleBar.height - } - - override fun setHeight(height: Float) { - titleBar.height = height - } - - override fun getProperties(): MutableMap { - return titleBar.properties - } - - override fun putProperties(m: MutableMap?) { - titleBar.putProperties(m) - } - - override fun putProperty(key: String?, value: Any?) { - titleBar.putProperty(key, value) - } - - override fun getLeftInset(): Float { - return titleBar.leftInset - } - - override fun getRightInset(): Float { - return titleBar.rightInset - } - - override fun forceHitTest(client: Boolean) { - titleBar.forceHitTest(client) - } - - override fun getContainingWindow(): Window { - return titleBar.containingWindow - } -} \ No newline at end of file diff --git a/src/main/kotlin/app/termora/MyFlatRootPaneUI.kt b/src/main/kotlin/app/termora/MyFlatRootPaneUI.kt new file mode 100644 index 0000000..93f3981 --- /dev/null +++ b/src/main/kotlin/app/termora/MyFlatRootPaneUI.kt @@ -0,0 +1,11 @@ +package app.termora + +import com.formdev.flatlaf.ui.FlatRootPaneUI +import com.formdev.flatlaf.ui.FlatTitlePane + +class MyFlatRootPaneUI : FlatRootPaneUI() { + + fun getTitlePane(): FlatTitlePane? { + return super.titlePane + } +} \ No newline at end of file diff --git a/src/main/kotlin/app/termora/OptionPane.kt b/src/main/kotlin/app/termora/OptionPane.kt index 926077d..7a3e99c 100644 --- a/src/main/kotlin/app/termora/OptionPane.kt +++ b/src/main/kotlin/app/termora/OptionPane.kt @@ -1,11 +1,13 @@ package app.termora +import app.termora.native.osx.NativeMacLibrary import com.formdev.flatlaf.FlatClientProperties import com.formdev.flatlaf.extras.components.FlatTextPane import com.formdev.flatlaf.util.SystemInfo import com.jetbrains.JBR import kotlinx.coroutines.* import kotlinx.coroutines.swing.Swing +import org.apache.commons.lang3.StringUtils import java.awt.BorderLayout import java.awt.Component import java.awt.Desktop @@ -113,6 +115,36 @@ object OptionPane { dialog.dispose() } + fun showInputDialog( + parentComponent: Component?, + title: String = UIManager.getString("OptionPane.messageDialogTitle"), + value: String = StringUtils.EMPTY, + placeholder: String = StringUtils.EMPTY, + ): String? { + val pane = JOptionPane(StringUtils.EMPTY, JOptionPane.PLAIN_MESSAGE, JOptionPane.OK_CANCEL_OPTION) + val dialog = initDialog(pane.createDialog(parentComponent, title)) + pane.wantsInput = true + pane.initialSelectionValue = value + + val textField = SwingUtils.getDescendantsOfType(JTextField::class.java, pane, true).firstOrNull() + if (textField?.name == "OptionPane.textField") { + textField.border = BorderFactory.createCompoundBorder( + BorderFactory.createMatteBorder(0, 0, 1, 0, DynamicColor.BorderColor), + BorderFactory.createEmptyBorder(0, 0, 2, 0) + ) + textField.background = UIManager.getColor("window") + textField.putClientProperty(FlatClientProperties.PLACEHOLDER_TEXT, placeholder) + } + + dialog.isVisible = true + dialog.dispose() + + val inputValue = pane.inputValue + if (inputValue == JOptionPane.UNINITIALIZED_VALUE) return null + + return inputValue as? String + } + fun openFileInFolder( parentComponent: Component, file: File, @@ -140,14 +172,31 @@ object OptionPane { } private fun initDialog(dialog: JDialog): JDialog { + if (SystemInfo.isWindows || SystemInfo.isLinux) { + dialog.rootPane.putClientProperty(FlatClientProperties.TITLE_BAR_SHOW_CLOSE, false) + dialog.rootPane.putClientProperty( + FlatClientProperties.TITLE_BAR_HEIGHT, + UIManager.getInt("TabbedPane.tabHeight") + ) + } else if (SystemInfo.isMacOS) { + dialog.rootPane.putClientProperty("apple.awt.windowTitleVisible", false) + dialog.rootPane.putClientProperty("apple.awt.fullWindowContent", true) + dialog.rootPane.putClientProperty("apple.awt.transparentTitleBar", true) + dialog.rootPane.putClientProperty( + FlatClientProperties.MACOS_WINDOW_BUTTONS_SPACING, + FlatClientProperties.MACOS_WINDOW_BUTTONS_SPACING_MEDIUM + ) - if (JBR.isWindowDecorationsSupported()) { - val windowDecorations = JBR.getWindowDecorations() - val titleBar = windowDecorations.createCustomTitleBar() - titleBar.putProperty("controls.visible", false) - titleBar.height = UIManager.getInt("TabbedPane.tabHeight") - if (SystemInfo.isMacOS) 10f else 6f - windowDecorations.setCustomTitleBar(dialog, titleBar) + val height = UIManager.getInt("TabbedPane.tabHeight") - 10 + if (JBR.isWindowDecorationsSupported()) { + val customTitleBar = JBR.getWindowDecorations().createCustomTitleBar() + customTitleBar.putProperty("controls.visible", false) + customTitleBar.height = height.toFloat() + JBR.getWindowDecorations().setCustomTitleBar(dialog, customTitleBar) + } else { + NativeMacLibrary.setControlsVisible(dialog, false) + } val label = JLabel(dialog.title) label.putClientProperty(FlatClientProperties.STYLE, "font: bold") @@ -155,11 +204,9 @@ object OptionPane { box.add(Box.createHorizontalGlue()) box.add(label) box.add(Box.createHorizontalGlue()) - box.preferredSize = Dimension(-1, titleBar.height.toInt()) - + box.preferredSize = Dimension(-1, height) dialog.contentPane.add(box, BorderLayout.NORTH) } - return dialog } } \ No newline at end of file diff --git a/src/main/kotlin/app/termora/TerminalTabbed.kt b/src/main/kotlin/app/termora/TerminalTabbed.kt index 944b5c4..48c0690 100644 --- a/src/main/kotlin/app/termora/TerminalTabbed.kt +++ b/src/main/kotlin/app/termora/TerminalTabbed.kt @@ -190,12 +190,11 @@ class TerminalTabbed( val rename = popupMenu.add(I18n.getString("termora.tabbed.contextmenu.rename")) rename.addActionListener { if (tabIndex > 0) { - val dialog = InputDialog( + val text = OptionPane.showInputDialog( SwingUtilities.getWindowAncestor(this), title = rename.text, - text = tabbedPane.getTitleAt(tabIndex), + value = tabbedPane.getTitleAt(tabIndex) ) - val text = dialog.getText() if (!text.isNullOrBlank()) { tabbedPane.setTitleAt(tabIndex, text) c.putClientProperty(titleProperty, text) diff --git a/src/main/kotlin/app/termora/TermoraFrame.kt b/src/main/kotlin/app/termora/TermoraFrame.kt index b78cf42..7b06cb0 100644 --- a/src/main/kotlin/app/termora/TermoraFrame.kt +++ b/src/main/kotlin/app/termora/TermoraFrame.kt @@ -7,21 +7,19 @@ import app.termora.actions.DataProviders import app.termora.sftp.SFTPTab import app.termora.terminal.DataKey import com.formdev.flatlaf.FlatClientProperties -import com.formdev.flatlaf.FlatLaf import com.formdev.flatlaf.util.SystemInfo import com.jetbrains.JBR +import java.awt.BorderLayout import java.awt.Dimension import java.awt.Insets import java.awt.event.MouseAdapter import java.awt.event.MouseEvent +import java.awt.event.MouseListener +import java.awt.event.MouseMotionListener import java.util.* import javax.imageio.ImageIO -import javax.swing.Box -import javax.swing.JFrame -import javax.swing.SwingUtilities +import javax.swing.* import javax.swing.SwingUtilities.isEventDispatchThread -import javax.swing.UIManager -import kotlin.math.max fun assertEventDispatchThread() { if (!isEventDispatchThread()) throw WrongThreadException("AWT EventQueue") @@ -33,14 +31,13 @@ class TermoraFrame : JFrame(), DataProvider { private val id = UUID.randomUUID().toString() private val windowScope = ApplicationScope.forWindowScope(this) - private val titleBar = LogicCustomTitleBar.createCustomTitleBar(this) private val tabbedPane = MyTabbedPane() - private val toolbar = TermoraToolBar(windowScope, titleBar, tabbedPane) + private val toolbar = TermoraToolBar(windowScope, this, tabbedPane) private val terminalTabbed = TerminalTabbed(windowScope, toolbar, tabbedPane) - private val isWindowDecorationsSupported by lazy { JBR.isWindowDecorationsSupported() } private val dataProviderSupport = DataProviderSupport() private val welcomePanel = WelcomePanel(windowScope) private val sftp get() = Database.getDatabase().sftp + private val myUI = MyFlatRootPaneUI() init { @@ -49,44 +46,146 @@ class TermoraFrame : JFrame(), DataProvider { } private fun initEvents() { + if (SystemInfo.isLinux) { + val mouseAdapter = object : MouseAdapter() { + override fun mouseClicked(e: MouseEvent) { + getMouseHandler()?.mouseClicked(e) + } - forceHitTest() + override fun mousePressed(e: MouseEvent) { + getMouseHandler()?.mousePressed(e) + } - // macos 需要判断是否全部删除 - // 当 Tab 为 0 的时候,需要加一个边距,避开控制栏 - if (SystemInfo.isMacOS && isWindowDecorationsSupported) { - tabbedPane.addChangeListener { - tabbedPane.leadingComponent = if (tabbedPane.tabCount == 0) { - Box.createHorizontalStrut(titleBar.leftInset.toInt()) - } else { - null + override fun mouseDragged(e: MouseEvent) { + val mouseLayer = getMouseLayer() ?: return + getMouseMotionListener()?.mouseDragged( + MouseEvent( + mouseLayer, + e.id, + e.`when`, + e.modifiersEx, + e.x, + e.y, + e.clickCount, + e.isPopupTrigger, + e.button + ) + ) + } + + private fun getMouseHandler(): MouseListener? { + return getHandler() as? MouseListener + } + + private fun getMouseMotionListener(): MouseMotionListener? { + return getHandler() as? MouseMotionListener + } + + private fun getMouseLayer(): JComponent? { + val titlePane = myUI.getTitlePane() ?: return null + val handlerField = titlePane.javaClass.getDeclaredField("mouseLayer") ?: return null + handlerField.isAccessible = true + return handlerField.get(titlePane) as? JComponent + } + + private fun getHandler(): Any? { + val titlePane = myUI.getTitlePane() ?: return null + val handlerField = titlePane.javaClass.getDeclaredField("handler") ?: return null + handlerField.isAccessible = true + return handlerField.get(titlePane) } } + toolbar.getJToolBar().addMouseListener(mouseAdapter) + toolbar.getJToolBar().addMouseMotionListener(mouseAdapter) } + /// force hit + if (SystemInfo.isMacOS) { + if (JBR.isWindowDecorationsSupported()) { + val height = UIManager.getInt("TabbedPane.tabHeight") + tabbedPane.tabAreaInsets.top + val customTitleBar = JBR.getWindowDecorations().createCustomTitleBar() + customTitleBar.height = height.toFloat() - // 监听主题变化 需要动态修改控制栏颜色 - if (SystemInfo.isWindows && isWindowDecorationsSupported) { - ThemeManager.getInstance().addThemeChangeListener(object : ThemeChangeListener { - override fun onChanged() { - titleBar.putProperty("controls.dark", FlatLaf.isLafDark()) + val mouseAdapter = object : MouseAdapter() { + + private fun hit(e: MouseEvent) { + if (e.source == tabbedPane) { + val index = tabbedPane.indexAtLocation(e.x, e.y) + if (index >= 0) { + return + } + } + customTitleBar.forceHitTest(false) + } + + override fun mouseClicked(e: MouseEvent) { + hit(e) + } + + override fun mousePressed(e: MouseEvent) { + hit(e) + } + + override fun mouseReleased(e: MouseEvent) { + hit(e) + } + + override fun mouseEntered(e: MouseEvent) { + hit(e) + } + + override fun mouseDragged(e: MouseEvent) { + hit(e) + } + + override fun mouseMoved(e: MouseEvent) { + hit(e) + } } - }) - } + terminalTabbed.addMouseListener(mouseAdapter) + terminalTabbed.addMouseMotionListener(mouseAdapter) + + tabbedPane.addMouseListener(mouseAdapter) + tabbedPane.addMouseMotionListener(mouseAdapter) + + toolbar.getJToolBar().addMouseListener(mouseAdapter) + toolbar.getJToolBar().addMouseMotionListener(mouseAdapter) + + JBR.getWindowDecorations().setCustomTitleBar(this, customTitleBar) + } + } } private fun initView() { - if (isWindowDecorationsSupported) { - titleBar.height = UIManager.getInt("TabbedPane.tabHeight").toFloat() - titleBar.putProperty("controls.dark", FlatLaf.isLafDark()) - JBR.getWindowDecorations().setCustomTitleBar(this, titleBar) + + // macOS 要避开左边的控制栏 + if (SystemInfo.isMacOS) { + tabbedPane.tabAreaInsets = Insets(0, 76, 0, 0) + } else if (SystemInfo.isWindows) { + // Windows 10 会有1像素误差 + tabbedPane.tabAreaInsets = Insets(if (SystemInfo.isWindows_11_orLater) 1 else 2, 2, 0, 0) + } else if (SystemInfo.isLinux) { + rootPane.setUI(myUI) + tabbedPane.tabAreaInsets = Insets(1, 2, 0, 0) } - if (SystemInfo.isLinux) { + val height = UIManager.getInt("TabbedPane.tabHeight") + tabbedPane.tabAreaInsets.top + + if (SystemInfo.isWindows || SystemInfo.isLinux) { rootPane.putClientProperty(FlatClientProperties.FULL_WINDOW_CONTENT, true) - rootPane.putClientProperty(FlatClientProperties.TITLE_BAR_HEIGHT, UIManager.getInt("TabbedPane.tabHeight")) + rootPane.putClientProperty(FlatClientProperties.TITLE_BAR_SHOW_ICON, false) + rootPane.putClientProperty(FlatClientProperties.TITLE_BAR_SHOW_TITLE, false) + rootPane.putClientProperty(FlatClientProperties.TITLE_BAR_HEIGHT, height) + } else if (SystemInfo.isMacOS) { + rootPane.putClientProperty("apple.awt.windowTitleVisible", false) + rootPane.putClientProperty("apple.awt.fullWindowContent", true) + rootPane.putClientProperty("apple.awt.transparentTitleBar", true) + rootPane.putClientProperty( + FlatClientProperties.MACOS_WINDOW_BUTTONS_SPACING, + FlatClientProperties.MACOS_WINDOW_BUTTONS_SPACING_MEDIUM + ) } if (SystemInfo.isWindows || SystemInfo.isLinux) { @@ -102,88 +201,21 @@ class TermoraFrame : JFrame(), DataProvider { terminalTabbed.addTerminalTab(welcomePanel) // 下一次事件循环检测是否固定 SFTP - SwingUtilities.invokeLater { - if (sftp.pinTab) { + if (sftp.pinTab) { + SwingUtilities.invokeLater { terminalTabbed.addTerminalTab(SFTPTab(), false) } } - // macOS 要避开左边的控制栏 - if (SystemInfo.isMacOS) { - val left = max(titleBar.leftInset.toInt(), 76) - if (tabbedPane.tabCount == 0) { - tabbedPane.leadingComponent = Box.createHorizontalStrut(left) - } else { - tabbedPane.tabAreaInsets = Insets(0, left, 0, 0) - } - } Disposer.register(windowScope, terminalTabbed) - add(terminalTabbed) + add(terminalTabbed, BorderLayout.CENTER) dataProviderSupport.addData(DataProviders.TabbedPane, tabbedPane) dataProviderSupport.addData(DataProviders.TermoraFrame, this) dataProviderSupport.addData(DataProviders.WindowScope, windowScope) } - - private fun forceHitTest() { - val mouseAdapter = object : MouseAdapter() { - - private fun hit(e: MouseEvent) { - if (e.source == tabbedPane) { - val index = tabbedPane.indexAtLocation(e.x, e.y) - if (index >= 0) { - return - } - } - titleBar.forceHitTest(false) - } - - override fun mouseClicked(e: MouseEvent) { - hit(e) - } - - override fun mousePressed(e: MouseEvent) { - if (e.source == toolbar.getJToolBar()) { - if (!isWindowDecorationsSupported && SwingUtilities.isLeftMouseButton(e)) { - if (JBR.isWindowMoveSupported()) { - JBR.getWindowMove().startMovingTogetherWithMouse(this@TermoraFrame, e.button) - } - } - } - hit(e) - } - - override fun mouseReleased(e: MouseEvent) { - hit(e) - } - - override fun mouseEntered(e: MouseEvent) { - hit(e) - } - - override fun mouseDragged(e: MouseEvent) { - - hit(e) - } - - override fun mouseMoved(e: MouseEvent) { - hit(e) - } - } - - - terminalTabbed.addMouseListener(mouseAdapter) - terminalTabbed.addMouseMotionListener(mouseAdapter) - - tabbedPane.addMouseListener(mouseAdapter) - tabbedPane.addMouseMotionListener(mouseAdapter) - - toolbar.getJToolBar().addMouseListener(mouseAdapter) - toolbar.getJToolBar().addMouseMotionListener(mouseAdapter) - } - override fun getData(dataKey: DataKey): T? { return dataProviderSupport.getData(dataKey) ?: terminalTabbed.getData(dataKey) @@ -204,5 +236,22 @@ class TermoraFrame : JFrame(), DataProvider { return id.hashCode() } + override fun addNotify() { + super.addNotify() + val dialog = object : DialogWrapper(this@TermoraFrame) { + init { + init() + controlsVisible = false + } + + override fun createCenterPanel(): JComponent { + return JPanel() + } + } + dialog.title = "Hello" + dialog.size = Dimension(800, 600) + dialog.setLocationRelativeTo(this) +// dialog.isVisible = true + } } \ No newline at end of file diff --git a/src/main/kotlin/app/termora/TermoraToolBar.kt b/src/main/kotlin/app/termora/TermoraToolBar.kt index 8c5fbb4..6b8b446 100644 --- a/src/main/kotlin/app/termora/TermoraToolBar.kt +++ b/src/main/kotlin/app/termora/TermoraToolBar.kt @@ -4,13 +4,13 @@ import app.termora.Application.ohMyJson import app.termora.actions.* import app.termora.findeverywhere.FindEverywhereAction import app.termora.snippet.SnippetAction +import com.formdev.flatlaf.FlatClientProperties import com.formdev.flatlaf.extras.components.FlatTabbedPane import com.formdev.flatlaf.util.SystemInfo -import com.jetbrains.WindowDecorations import kotlinx.serialization.Serializable import org.apache.commons.lang3.StringUtils import org.jdesktop.swingx.action.ActionContainerFactory -import java.awt.Insets +import java.awt.Rectangle import java.awt.event.ComponentAdapter import java.awt.event.ComponentEvent import javax.swing.Box @@ -25,7 +25,7 @@ data class ToolBarAction( class TermoraToolBar( private val windowScope: WindowScope, - private val titleBar: WindowDecorations.CustomTitleBar, + private val frame: TermoraFrame, private val tabbedPane: FlatTabbedPane ) { private val properties by lazy { Database.getDatabase().properties } @@ -155,14 +155,11 @@ class TermoraToolBar( } fun adjust() { - if (SystemInfo.isMacOS) { - val left = titleBar.leftInset.toInt() - if (tabbedPane.tabAreaInsets.left != left) { - tabbedPane.tabAreaInsets = Insets(0, left, 0, 0) - } - } else if (SystemInfo.isWindows || SystemInfo.isLinux) { - - val right = titleBar.rightInset.toInt() + if (SystemInfo.isWindows || SystemInfo.isLinux) { + val rectangle = + frame.rootPane.getClientProperty(FlatClientProperties.FULL_WINDOW_CONTENT_BUTTONS_BOUNDS) + as? Rectangle ?: return + val right = rectangle.width val toolbar = this@MyToolBar for (i in 0 until toolbar.componentCount) { val c = toolbar.getComponent(i) diff --git a/src/main/kotlin/app/termora/findeverywhere/FindEverywhere.kt b/src/main/kotlin/app/termora/findeverywhere/FindEverywhere.kt index 3352097..f8fc2ab 100644 --- a/src/main/kotlin/app/termora/findeverywhere/FindEverywhere.kt +++ b/src/main/kotlin/app/termora/findeverywhere/FindEverywhere.kt @@ -7,9 +7,7 @@ import app.termora.WindowScope import app.termora.actions.AnAction import app.termora.actions.AnActionEvent import app.termora.macro.MacroFindEverywhereProvider -import com.formdev.flatlaf.FlatClientProperties import com.formdev.flatlaf.extras.components.FlatTextField -import com.jetbrains.JBR import java.awt.BorderLayout import java.awt.Dimension import java.awt.Insets @@ -45,17 +43,10 @@ class FindEverywhere(owner: Window, windowScope: WindowScope) : DialogWrapper(ow minimumSize = Dimension(size.width / 2, size.height / 2) isModal = false lostFocusDispose = true - controlsVisible = false - defaultCloseOperation = WindowConstants.DISPOSE_ON_CLOSE setLocationRelativeTo(null) - // 不支持装饰,铺满 - if (!JBR.isWindowDecorationsSupported()) { - rootPane.putClientProperty(FlatClientProperties.FULL_WINDOW_CONTENT, true) - rootPane.putClientProperty(FlatClientProperties.TITLE_BAR_SHOW_CLOSE, false) - } - rootPane.background = DynamicColor("desktop") + val desktopBackground = DynamicColor("desktop") centerPanel.background = DynamicColor("desktop") centerPanel.border = BorderFactory.createEmptyBorder(12, 12, 12, 12) @@ -70,7 +61,7 @@ class FindEverywhere(owner: Window, windowScope: WindowScope) : DialogWrapper(ow resultList.isRolloverEnabled = false resultList.selectionMode = ListSelectionModel.SINGLE_SELECTION resultList.border = BorderFactory.createEmptyBorder(5, 0, 0, 0) - resultList.background = rootPane.background + resultList.background = desktopBackground val scrollPane = JScrollPane(resultList) @@ -226,5 +217,11 @@ class FindEverywhere(owner: Window, windowScope: WindowScope) : DialogWrapper(ow super.setVisible(visible) } + override fun addNotify() { + super.addNotify() + + controlsVisible = false + fullWindowContent = true + } } diff --git a/src/main/kotlin/app/termora/keymap/KeymapPanel.kt b/src/main/kotlin/app/termora/keymap/KeymapPanel.kt index c8879ac..2c54435 100644 --- a/src/main/kotlin/app/termora/keymap/KeymapPanel.kt +++ b/src/main/kotlin/app/termora/keymap/KeymapPanel.kt @@ -119,10 +119,11 @@ class KeymapPanel : JPanel(BorderLayout()) { val keymap = getCurrentKeymap() val index = keymapComboBox.selectedIndex if (keymap != null && !keymap.isReadonly && index >= 0) { - val text = InputDialog( + val text = OptionPane.showInputDialog( SwingUtilities.getWindowAncestor(this@KeymapPanel), - title = renameBtn.toolTipText, text = keymap.name - ).getText() + title = renameBtn.toolTipText, + value = keymap.name + ) if (!text.isNullOrBlank()) { if (text != keymap.name) { keymapManager.removeKeymap(keymap.name) diff --git a/src/main/kotlin/app/termora/keymgr/KeyManagerPanel.kt b/src/main/kotlin/app/termora/keymgr/KeyManagerPanel.kt index f0cbc48..b624b71 100644 --- a/src/main/kotlin/app/termora/keymgr/KeyManagerPanel.kt +++ b/src/main/kotlin/app/termora/keymgr/KeyManagerPanel.kt @@ -103,7 +103,9 @@ class KeyManagerPanel : JPanel(BorderLayout()) { private fun initEvents() { generateBtn.addActionListener { - val dialog = GenerateKeyDialog(SwingUtilities.getWindowAncestor(this)) + val owner = SwingUtilities.getWindowAncestor(this) + val dialog = GenerateKeyDialog(owner) + dialog.setLocationRelativeTo(owner) dialog.isVisible = true if (dialog.ohKeyPair != OhKeyPair.empty) { val keyPair = dialog.ohKeyPair @@ -142,12 +144,14 @@ class KeyManagerPanel : JPanel(BorderLayout()) { editBtn.addActionListener { val row = keyPairTable.selectedRow if (row >= 0) { + val owner = SwingUtilities.getWindowAncestor(this) var ohKeyPair = keyPairTableModel.getOhKeyPair(row) val dialog = GenerateKeyDialog( - SwingUtilities.getWindowAncestor(this), + owner, ohKeyPair, true ) + dialog.setLocationRelativeTo(owner) dialog.title = ohKeyPair.name dialog.isVisible = true ohKeyPair = dialog.ohKeyPair @@ -342,7 +346,6 @@ class KeyManagerPanel : JPanel(BorderLayout()) { pack() size = Dimension(UIManager.getInt("Dialog.width") - 300, size.height) - setLocationRelativeTo(null) } @@ -354,7 +357,7 @@ class KeyManagerPanel : JPanel(BorderLayout()) { var rows = 1 val step = 2 - return FormBuilder.create().layout(layout).padding("0dlu, $formMargin, $formMargin, $formMargin") + return FormBuilder.create().layout(layout).padding("2dlu, $formMargin, $formMargin, $formMargin") .add("${I18n.getString("termora.keymgr.table.type")}:").xy(1, rows) .add(typeComboBox).xy(3, rows).apply { rows += step } .add("${I18n.getString("termora.keymgr.table.length")}:").xy(1, rows) @@ -512,7 +515,7 @@ class KeyManagerPanel : JPanel(BorderLayout()) { var rows = 1 val step = 2 - return FormBuilder.create().layout(layout).padding("0dlu, $formMargin, $formMargin, $formMargin") + return FormBuilder.create().layout(layout).padding("2dlu, $formMargin, $formMargin, $formMargin") .add("File:").xy(1, rows) .add(fileTextField).xy(3, rows).apply { rows += step } .add("${I18n.getString("termora.keymgr.table.type")}:").xy(1, rows) @@ -587,8 +590,10 @@ class KeyManagerPanel : JPanel(BorderLayout()) { try { val provider = FileKeyPairProvider(file.toPath()) provider.passwordFinder = FilePasswordProvider { _, _, _ -> - val dialog = InputDialog(owner = this@ImportKeyDialog, title = "Password") - dialog.getText() ?: String() + OptionPane.showInputDialog( + SwingUtilities.getWindowAncestor(this), + title = I18n.getString("termora.new-host.general.password"), + ) ?: String() } val keyPair = provider.loadKeys(null).firstOrNull() ?: throw IllegalStateException("Failed to load the key file") diff --git a/src/main/kotlin/app/termora/macro/MacroDialog.kt b/src/main/kotlin/app/termora/macro/MacroDialog.kt index b039d8c..320c96e 100644 --- a/src/main/kotlin/app/termora/macro/MacroDialog.kt +++ b/src/main/kotlin/app/termora/macro/MacroDialog.kt @@ -83,8 +83,11 @@ class MacroDialog(owner: Window) : DialogWrapper(owner) { val index = list.selectedIndex if (index >= 0) { val macro = model.getElementAt(index) - val dialog = InputDialog(owner = this, title = macro.name, text = macro.name) - val text = dialog.getText() ?: String() + val text = OptionPane.showInputDialog( + this, + title = macro.name, + value = macro.name + ) ?: String() if (text.isNotBlank()) { val newMacro = macro.copy(name = text) macroManager.addMacro(newMacro) diff --git a/src/main/kotlin/app/termora/native/osx/NativeMacLibrary.kt b/src/main/kotlin/app/termora/native/osx/NativeMacLibrary.kt new file mode 100644 index 0000000..9134966 --- /dev/null +++ b/src/main/kotlin/app/termora/native/osx/NativeMacLibrary.kt @@ -0,0 +1,51 @@ +package app.termora.native.osx + +import de.jangassen.jfa.foundation.Foundation +import de.jangassen.jfa.foundation.ID +import org.slf4j.LoggerFactory +import java.awt.Component +import java.awt.Window + +object NativeMacLibrary { + private val log = LoggerFactory.getLogger(NativeMacLibrary::class.java) + + fun getNSWindow(window: Window): Long? { + try { + val peerField = Component::class.java.getDeclaredField("peer") ?: return null + peerField.isAccessible = true + val peer = peerField.get(window) ?: return null + + val platformWindowField = peer.javaClass.getDeclaredField("platformWindow") ?: return null + platformWindowField.isAccessible = true + val platformWindow = platformWindowField.get(peer) + + val ptrField = Class.forName("sun.lwawt.macosx.CFRetainedResource") + .getDeclaredField("ptr") ?: return null + ptrField.isAccessible = true + return ptrField.get(platformWindow) as Long + } catch (e: Exception) { + if (log.isErrorEnabled) { + log.error(e.message, e) + } + return null + } + } + + fun setControlsVisible(window: Window, visible: Boolean) { + val nsWindow = ID(getNSWindow(window) ?: return) + try { + Foundation.executeOnMainThread(true, true) { + for (i in 0..2) { + val button = Foundation.invoke(nsWindow, "standardWindowButton:", i) + Foundation.invoke(button, "setHidden:", !visible) + } + } + } catch (e: Exception) { + if (log.isErrorEnabled) { + log.error(e.message, e) + } + } + } + + +} \ No newline at end of file diff --git a/src/main/kotlin/app/termora/sftp/FileSystemViewTable.kt b/src/main/kotlin/app/termora/sftp/FileSystemViewTable.kt index 1013b85..d1e2790 100644 --- a/src/main/kotlin/app/termora/sftp/FileSystemViewTable.kt +++ b/src/main/kotlin/app/termora/sftp/FileSystemViewTable.kt @@ -414,12 +414,11 @@ class FileSystemViewTable( val index = selectedRow if (index < 0) return val attr = model.getAttr(index) - val dialog = InputDialog( + val text = OptionPane.showInputDialog( owner, - title = attr.name, - text = attr.name, - ) - val text = dialog.getText() ?: return + value = attr.name, + title = I18n.getString("termora.transport.table.contextmenu.rename") + ) ?: return if (text.isBlank() || text == attr.name) return if (model.getPathNames().contains(text)) { OptionPane.showMessageDialog( @@ -544,12 +543,7 @@ class FileSystemViewTable( private fun newFolderOrFile(isFile: Boolean) { val name = if (isFile) I18n.getString("termora.transport.table.contextmenu.new.file") else I18n.getString("termora.welcome.contextmenu.new.folder.name") - val dialog = InputDialog( - owner, - title = name, - text = name, - ) - val text = dialog.getText() ?: return + val text = OptionPane.showInputDialog(owner, title = name, value = name) ?: return if (text.isBlank()) return if (model.getPathNames().contains(text)) { OptionPane.showMessageDialog(