From 6496f5f5aeb642c3a7731a122614cd95e38684a9 Mon Sep 17 00:00:00 2001 From: Manty Date: Sun, 22 Feb 2026 04:18:46 +0900 Subject: [PATCH] =?UTF-8?q?=E2=8F=BA=20[client]=20Embedded=20JRE,=20Worksp?= =?UTF-8?q?ace=20management,=20Import=20perspective=20setting=20from=20git?= =?UTF-8?q?,=20Dark=20Mode=20=20=20-=20Embed=20JRE=20in=20distribution=20s?= =?UTF-8?q?o=20users=20no=20longer=20need=20to=20install=20Java=20separate?= =?UTF-8?q?ly=20=20=20=20=20-=20Bundle=20JustJ=20OpenJDK=2021=20JRE=20into?= =?UTF-8?q?=20platform-specific=20packages=20(macOS,=20Linux,=20Windows)?= =?UTF-8?q?=20=20=20-=20Add=20launcher=20scripts=20that=20detect=20bundled?= =?UTF-8?q?=20JRE=20and=20fall=20back=20to=20system=20Java=20=20=20-=20Add?= =?UTF-8?q?=20workspace=20switch/create/manage=20functionality=20with=20re?= =?UTF-8?q?launch=20support=20=20=20-=20Add=20import=20workspace=20from=20?= =?UTF-8?q?file=20and=20GitHub=20with=20auto-update=20check=20=20=20-=20Ap?= =?UTF-8?q?ply=20dark=20mode=20colors=20to=20XYGraph,=20PlotArea,=20Legend?= =?UTF-8?q?,=20EQ=20views,=20and=20XLog=20views?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .claude/settings.local.json | 0 .gitignore | 4 + scouter.client.build/pom.xml | 36 +- scouter.client.product/pom.xml | 5 + scouter.client.product/scouter.client.product | 6 +- .../scripts/run-scouter.command | 30 +- scouter.client.product/scripts/run-scouter.sh | 22 + scouter.client/plugin.xml | 37 +- .../csstudio/swt/xygraph/figures/Legend.java | 3 +- .../swt/xygraph/figures/PlotArea.java | 6 +- .../csstudio/swt/xygraph/figures/XYGraph.java | 29 +- .../src/scouter/client/Application.java | 75 +- .../client/ApplicationActionBarAdvisor.java | 4 +- .../ApplicationWorkbenchWindowAdvisor.java | 150 ++- .../scouter/client/PerspectiveService.java | 46 +- .../ImportWorkspaceFromFileAction.java | 83 ++ .../ImportWorkspaceFromGitHubAction.java | 45 + .../client/actions/SwitchWorkspaceAction.java | 95 ++ .../counter/views/CounterAllPairPainter.java | 17 +- .../client/model/AgentColorManager.java | 5 +- .../client/popup/ImportFromGitHubDialog.java | 1165 +++++++++++++++++ .../scouter/client/popup/LoginDialog2.java | 26 +- .../scouter/client/preferences/PManager.java | 2 + .../preferences/PreferenceConstants.java | 4 +- .../src/scouter/client/util/ColorUtil.java | 207 ++- .../scouter/client/views/EQCommonView.java | 67 +- .../client/views/VerticalEQCommonView.java | 55 +- .../workspace/SwitchWorkspaceDialog.java | 271 ++++ .../client/workspace/WorkspaceInfo.java | 54 + .../client/workspace/WorkspaceManager.java | 203 +++ .../src/scouter/client/xlog/ImageCache.java | 19 +- .../xlog/views/XLogFullProfileView.java | 17 +- .../client/xlog/views/XLogProfileView.java | 8 +- .../xlog/views/XLogThreadProfileView.java | 8 +- .../client/xlog/views/XLogViewCommon.java | 2 + .../client/xlog/views/XLogViewPainter.java | 39 +- 36 files changed, 2650 insertions(+), 195 deletions(-) create mode 100644 .claude/settings.local.json create mode 100755 scouter.client.product/scripts/run-scouter.sh create mode 100644 scouter.client/src/scouter/client/actions/ImportWorkspaceFromFileAction.java create mode 100644 scouter.client/src/scouter/client/actions/ImportWorkspaceFromGitHubAction.java create mode 100644 scouter.client/src/scouter/client/actions/SwitchWorkspaceAction.java create mode 100644 scouter.client/src/scouter/client/popup/ImportFromGitHubDialog.java create mode 100644 scouter.client/src/scouter/client/workspace/SwitchWorkspaceDialog.java create mode 100644 scouter.client/src/scouter/client/workspace/WorkspaceInfo.java create mode 100644 scouter.client/src/scouter/client/workspace/WorkspaceManager.java diff --git a/.claude/settings.local.json b/.claude/settings.local.json new file mode 100644 index 000000000..e69de29bb diff --git a/.gitignore b/.gitignore index 65e944fa8..0c4a0a519 100644 --- a/.gitignore +++ b/.gitignore @@ -114,3 +114,7 @@ scouter.agent.batch/.settings/org.eclipse.core.resources.prefs /scouter.webapp/conf/scouterConfSample3.conf scouter.server.jar AGENTS.md + +.claude +*/.DS_Store +.sdkmanrc \ No newline at end of file diff --git a/scouter.client.build/pom.xml b/scouter.client.build/pom.xml index cb293d8e9..50b759e2f 100644 --- a/scouter.client.build/pom.xml +++ b/scouter.client.build/pom.xml @@ -1,5 +1,5 @@ - 4.0.0 @@ -16,13 +16,18 @@ 5.0.1 UTF-8 - + eclipse-2025-12 p2 https://download.eclipse.org/releases/2025-12/ - + + justj-jre-21 + p2 + https://download.eclipse.org/justj/jres/21/updates/release/latest + + @@ -33,16 +38,21 @@ ${tycho-version} true - - org.eclipse.tycho - target-platform-configuration - ${tycho-version} - - - - win32 - win32 - x86_64 + + org.eclipse.tycho + target-platform-configuration + ${tycho-version} + + + + true + + + + + win32 + win32 + x86_64 linux diff --git a/scouter.client.product/pom.xml b/scouter.client.product/pom.xml index f196fd0d0..b4ac06be5 100644 --- a/scouter.client.product/pom.xml +++ b/scouter.client.product/pom.xml @@ -86,9 +86,14 @@ + + + diff --git a/scouter.client.product/scouter.client.product b/scouter.client.product/scouter.client.product index 9833b1e67..060a1e7b6 100644 --- a/scouter.client.product/scouter.client.product +++ b/scouter.client.product/scouter.client.product @@ -23,8 +23,8 @@ -data @user.home/scouter - -Xms128m --Xmx1024m + -Xms1g +-Xmx2g -XX:+UseG1GC -Dosgi.requiredJavaVersion=21 @@ -60,6 +60,8 @@ + + diff --git a/scouter.client.product/scripts/run-scouter.command b/scouter.client.product/scripts/run-scouter.command index 04e7dd9ab..a3ce4a66f 100644 --- a/scouter.client.product/scripts/run-scouter.command +++ b/scouter.client.product/scripts/run-scouter.command @@ -1,7 +1,9 @@ #!/bin/bash # # Scouter Client Launcher for macOS -# 이 스크립트는 quarantine 속성을 제거하고 Scouter Client를 실행합니다. +# 번들 JRE(JustJ)가 scouter.ini에 설정되어 있으므로 +# 네이티브 런처가 자동으로 번들 JRE를 사용합니다. +# 이 스크립트는 번들 JRE가 없는 경우에만 시스템 Java를 확인합니다. # SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" @@ -10,17 +12,33 @@ APP_PATH="$SCRIPT_DIR/$APP_NAME" # 앱이 존재하는지 확인 if [ ! -d "$APP_PATH" ]; then - echo "❌ 오류: $APP_NAME 을 찾을 수 없습니다." - echo "이 스크립트를 $APP_NAME 과 같은 폴더에 위치시켜 주세요." + echo "Error: $APP_NAME not found." + echo "Please place this script in the same folder as $APP_NAME." read -p "Press Enter to exit..." exit 1 fi +# 번들 JRE 확인 (plugins 하위에 JustJ JRE가 배치됨) +BUNDLED_JRE=$(find "$APP_PATH/Contents/Eclipse/plugins" -path "*/justj*/jre/bin/java" 2>/dev/null | head -1) +if [ -z "$BUNDLED_JRE" ]; then + # 시스템 Java 확인 + if ! command -v java &> /dev/null; then + echo "Error: Java not found. Please install Java 21+." + echo " Recommended: https://adoptium.net/" + read -p "Press Enter to exit..." + exit 1 + fi + JAVA_VER=$(java -version 2>&1 | head -1 | sed 's/.*"\(.*\)".*/\1/' | cut -d. -f1) + if [ "$JAVA_VER" -lt 21 ] 2>/dev/null; then + echo "Error: Java 21+ required. Current: Java $JAVA_VER" + echo " Recommended: https://adoptium.net/" + read -p "Press Enter to exit..." + exit 1 + fi +fi + # quarantine 속성 제거 -echo "🔓 보안 속성을 제거하는 중..." xattr -cr "$APP_PATH" # 앱 실행 -echo "🚀 Scouter Client를 실행합니다..." open "$APP_PATH" - diff --git a/scouter.client.product/scripts/run-scouter.sh b/scouter.client.product/scripts/run-scouter.sh new file mode 100755 index 000000000..10c48a11f --- /dev/null +++ b/scouter.client.product/scripts/run-scouter.sh @@ -0,0 +1,22 @@ +#!/bin/bash +# +# Scouter Client Launcher for Linux +# 번들 JRE(JustJ)가 scouter.ini에 설정되어 있으므로 +# 네이티브 런처가 자동으로 번들 JRE를 사용합니다. +# 이 스크립트는 번들 JRE가 없는 경우에만 시스템 Java를 확인합니다. +# + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" + +# 번들 JRE 확인 (plugins 하위에 JustJ JRE가 배치됨) +BUNDLED_JRE=$(find "$SCRIPT_DIR/plugins" -path "*/justj*/jre/bin/java" 2>/dev/null | head -1) +if [ -z "$BUNDLED_JRE" ]; then + # 시스템 Java 확인 + if ! command -v java &> /dev/null; then + echo "Error: Java not found. Please install Java 21+." + echo " Recommended: https://adoptium.net/" + exit 1 + fi +fi + +exec "$SCRIPT_DIR/scouter" "$@" diff --git a/scouter.client/plugin.xml b/scouter.client/plugin.xml index db22eb6f1..36780c90f 100644 --- a/scouter.client/plugin.xml +++ b/scouter.client/plugin.xml @@ -1194,6 +1194,24 @@ id="scouter.client.actions.OpenServerManagerAction" name="Open ServerManager"> + + + + + + @@ -1231,9 +1249,24 @@ label="Export perspective settings" style="push"> + + + + + + + false addListenerToBoldline(); // Added by scouter.project diff --git a/scouter.client/src/org/csstudio/swt/xygraph/figures/PlotArea.java b/scouter.client/src/org/csstudio/swt/xygraph/figures/PlotArea.java index 24a651139..a3bd3362e 100644 --- a/scouter.client/src/org/csstudio/swt/xygraph/figures/PlotArea.java +++ b/scouter.client/src/org/csstudio/swt/xygraph/figures/PlotArea.java @@ -19,6 +19,7 @@ import org.csstudio.swt.xygraph.util.SWTConstants; import org.csstudio.swt.xygraph.util.XYGraphMediaFactory; import org.csstudio.swt.xygraph.util.XYGraphMediaFactory.CURSOR_TYPE; +import scouter.client.util.ColorUtil; import org.eclipse.draw2d.Figure; import org.eclipse.draw2d.Graphics; import org.eclipse.draw2d.MouseEvent; @@ -91,9 +92,8 @@ public void removePropertyChangeListener(String property, public PlotArea(final XYGraph xyGraph) { this.xyGraph = xyGraph; - setBackgroundColor(XYGraphMediaFactory.getInstance().getColor(255, 255, - 255)); - setForegroundColor(XYGraphMediaFactory.getInstance().getColor(0, 0, 0)); + setBackgroundColor(ColorUtil.getChartBackground()); + setForegroundColor(ColorUtil.getChartForeground()); setOpaque(true); RGB backRGB = getBackgroundColor().getRGB(); revertBackColor = XYGraphMediaFactory.getInstance().getColor( diff --git a/scouter.client/src/org/csstudio/swt/xygraph/figures/XYGraph.java b/scouter.client/src/org/csstudio/swt/xygraph/figures/XYGraph.java index b26418d5b..89a353326 100644 --- a/scouter.client/src/org/csstudio/swt/xygraph/figures/XYGraph.java +++ b/scouter.client/src/org/csstudio/swt/xygraph/figures/XYGraph.java @@ -25,6 +25,7 @@ import org.csstudio.swt.xygraph.util.Log10; import org.csstudio.swt.xygraph.util.SingleSourceHelper; import org.csstudio.swt.xygraph.util.XYGraphMediaFactory; +import scouter.client.util.ColorUtil; import org.eclipse.draw2d.Figure; import org.eclipse.draw2d.Graphics; import org.eclipse.draw2d.Label; @@ -125,6 +126,26 @@ public void setXyGraphMem(XYGraphMemento xyGraphMem) { new RGB(219, 128, 4), // orange }; + final public static RGB[] DEFAULT_TRACES_COLOR_DARK = + { + new RGB( 80, 140, 255), // blue + new RGB(255, 80, 80), // red + new RGB( 80, 220, 80), // green + new RGB(200, 200, 210), // light gray (replaces black) + new RGB(180, 100, 255), // violett + new RGB(255, 200, 60), // yellow + new RGB(255, 80, 240), // pink + new RGB(255, 170, 170), // peachy + new RGB( 80, 255, 80), // neon green + new RGB( 80, 230, 255), // neon blue + new RGB(200, 140, 60), // brown + new RGB(255, 180, 60), // orange + }; + + public static RGB[] getDefaultTracesColor() { + return ColorUtil.isDarkMode() ? DEFAULT_TRACES_COLOR_DARK : DEFAULT_TRACES_COLOR; + } + private int traceNum = 0; private boolean transparent = false; private boolean showLegend = true; @@ -190,6 +211,11 @@ public XYGraph() { primaryXAxis.setTickLableSide(LabelSide.Primary); addAxis(primaryXAxis); + primaryXAxis.setForegroundColor(ColorUtil.getChartForeground()); + primaryYAxis.setForegroundColor(ColorUtil.getChartForeground()); + primaryXAxis.setMajorGridColor(ColorUtil.getAxisGridColor()); + primaryYAxis.setMajorGridColor(ColorUtil.getAxisGridColor()); + operationsManager = new OperationsManager(); } @@ -436,8 +462,9 @@ public boolean removeAxis(Axis axis){ public void addTrace(Trace trace){ if (trace.getTraceColor() == null) { // Cycle through default colors + RGB[] colors = getDefaultTracesColor(); trace.setTraceColor(XYGraphMediaFactory.getInstance().getColor( - DEFAULT_TRACES_COLOR[traceNum % DEFAULT_TRACES_COLOR.length])); + colors[traceNum % colors.length])); ++traceNum; } if(legendMap.containsKey(trace.getYAxis())) diff --git a/scouter.client/src/scouter/client/Application.java b/scouter.client/src/scouter/client/Application.java index 9aed01e86..b4a166c6a 100644 --- a/scouter.client/src/scouter/client/Application.java +++ b/scouter.client/src/scouter/client/Application.java @@ -33,6 +33,7 @@ import scouter.client.server.Server; import scouter.client.server.ServerManager; import scouter.client.util.ClientFileUtil; +import scouter.client.workspace.WorkspaceManager; import java.io.File; import java.io.IOException; @@ -49,11 +50,20 @@ public class Application implements IApplication { public Object start(IApplicationContext context) throws Exception { Location instanceLocation = Platform.getInstanceLocation(); -// if(instanceLocation.isSet()) -// instanceLocation.release(); -// instanceLocation.set(new URL("file", null, System.getProperty("user.home") + "/scouter-workspace-test"), false); - + + String lastUsedPath = WorkspaceManager.getInstance().getLastUsedWorkspacePath(); + if (lastUsedPath != null) { + String currentPath = instanceLocation.getURL().getFile(); + if (!normalizePath(currentPath).equals(normalizePath(lastUsedPath)) + && new File(lastUsedPath).isDirectory()) { + String commandLine = buildRestoreCommandLine(lastUsedPath); + System.setProperty("eclipse.exitdata", commandLine); + return EXIT_RELAUNCH; + } + } + String workspaceRootName = instanceLocation.getURL().getFile(); + WorkspaceManager.getInstance().registerCurrentWorkspace(workspaceRootName); String importWorkingDirName = workspaceRootName + separator+ "import-working"; try { @@ -92,7 +102,8 @@ private boolean openLoginDialog(Display display) { ServerPrefUtil.storeDefaultServer(server.getIp()+":"+server.getPort()); ServerManager.getInstance().setDefaultServer(server); }, LoginDialog2.TYPE_STARTUP, null, null); - return (dialog.open() == Window.OK); + int result = dialog.open(); + return (result == Window.OK || result == LoginDialog2.SKIP_LOGIN); } @@ -154,12 +165,60 @@ private boolean loginAutomaticallyWhenAutoLoginEnabled() { return autoLogined; } + private static final Object EXIT_RELAUNCH = Integer.valueOf(24); + private Object createAndRunWorkbench(Display display) { int returnCode = PlatformUI.createAndRunWorkbench(display, new ApplicationWorkbenchAdvisor()); - if (returnCode == PlatformUI.RETURN_RESTART) + if (returnCode == PlatformUI.RETURN_RESTART) { + if ("true".equals(System.getProperty("scouter.workspace.switch"))) { + System.clearProperty("scouter.workspace.switch"); + return EXIT_RELAUNCH; + } return IApplication.EXIT_RESTART; - else - return IApplication.EXIT_OK; + } + return IApplication.EXIT_OK; + } + + private String buildRestoreCommandLine(String newWorkspacePath) { + String property = System.getProperty("eclipse.commands"); + if (property == null) { + return "-data\n" + newWorkspacePath + "\n"; + } + + StringBuilder result = new StringBuilder(); + String[] lines = property.split("\n"); + boolean skipNext = false; + boolean dataFound = false; + + for (String line : lines) { + if (skipNext) { + skipNext = false; + continue; + } + if ("-data".equals(line.trim())) { + result.append("-data\n"); + result.append(newWorkspacePath).append("\n"); + skipNext = true; + dataFound = true; + } else { + result.append(line).append("\n"); + } + } + + if (!dataFound) { + result.append("-data\n"); + result.append(newWorkspacePath).append("\n"); + } + + return result.toString(); + } + + private static String normalizePath(String path) { + if (path == null) return ""; + if (path.endsWith("/") || path.endsWith(File.separator)) { + path = path.substring(0, path.length() - 1); + } + return path; } public void stop() { diff --git a/scouter.client/src/scouter/client/ApplicationActionBarAdvisor.java b/scouter.client/src/scouter/client/ApplicationActionBarAdvisor.java index 8290e32db..8e4f973e1 100644 --- a/scouter.client/src/scouter/client/ApplicationActionBarAdvisor.java +++ b/scouter.client/src/scouter/client/ApplicationActionBarAdvisor.java @@ -47,7 +47,9 @@ protected void makeActions(IWorkbenchWindow window) { register(new OpenClientEnvViewAction(window)); register(new OpenWorkspaceExplorerAction(window, "Workspace Explorer", Images.explorer, serverId)); register(new ExportWorkspaceAction(window, "Export perspective settings", Images.explorer)); - register(new ImportWorkspaceAction(window, "Import perspective settings", Images.explorer)); + register(new ImportWorkspaceFromFileAction(window, "File", Images.explorer)); + register(new ImportWorkspaceFromGitHubAction(window, "GitHub", Images.explorer)); + register(new SwitchWorkspaceAction(window, "Switch Workspace")); register(new RestartAction(window, "Restart")); // Management diff --git a/scouter.client/src/scouter/client/ApplicationWorkbenchWindowAdvisor.java b/scouter.client/src/scouter/client/ApplicationWorkbenchWindowAdvisor.java index 45c4dbcd8..68fb5cd91 100644 --- a/scouter.client/src/scouter/client/ApplicationWorkbenchWindowAdvisor.java +++ b/scouter.client/src/scouter/client/ApplicationWorkbenchWindowAdvisor.java @@ -1,8 +1,8 @@ /* - * Copyright 2015 the original author or authors. + * Copyright 2015 the original author or authors. * @https://github.com/scouter-project/scouter * - * Licensed under the Apache License, Version 2.0 (the "License"); + * 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 * @@ -12,7 +12,7 @@ * 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. + * limitations under the License. * */ package scouter.client; @@ -22,11 +22,14 @@ import org.eclipse.jface.action.IContributionItem; import org.eclipse.jface.action.MenuManager; import org.eclipse.swt.graphics.Point; +import org.eclipse.jface.dialogs.MessageDialog; import org.eclipse.swt.widgets.Display; -import org.eclipse.ui.IWorkbenchWindow; +import org.eclipse.swt.widgets.Shell; +import org.eclipse.ui.PlatformUI; import org.eclipse.ui.WorkbenchException; import org.eclipse.ui.application.ActionBarAdvisor; import org.eclipse.ui.application.IActionBarConfigurer; +import org.eclipse.ui.IWorkbenchWindow; import org.eclipse.ui.application.IWorkbenchWindowConfigurer; import org.eclipse.ui.application.WorkbenchWindowAdvisor; import org.eclipse.ui.internal.WorkbenchPlugin; @@ -36,9 +39,13 @@ import scouter.Version; import scouter.client.misc.UpdateCheckScheduler; import scouter.client.notice.NoticeCheckScheduler; +import scouter.client.popup.ImportFromGitHubDialog; import scouter.client.remote.CheckMyJob; import scouter.client.threads.AlertProxyThread; import scouter.client.threads.SessionObserver; +import scouter.client.workspace.WorkspaceManager; +import scouter.client.util.ExUtil; +import scouter.util.ThreadUtil; import java.util.TimeZone; @@ -55,10 +62,10 @@ */ public class ApplicationWorkbenchWindowAdvisor extends WorkbenchWindowAdvisor { - + private ApplicationActionBarAdvisor actionBarAdvisor; Display display; - + public ApplicationWorkbenchWindowAdvisor( IWorkbenchWindowConfigurer configurer) { super(configurer); @@ -70,9 +77,9 @@ public ActionBarAdvisor createActionBarAdvisor( return actionBarAdvisor; } - + IWorkbenchWindowConfigurer configurer; - + @SuppressWarnings("restriction") public void preWindowOpen() { removeUnwantedActionSets(); @@ -82,62 +89,95 @@ public void preWindowOpen() { configurer.setShowMenuBar(true); configurer.setShowCoolBar(false); configurer.setShowStatusLine(true); - //PlatformUI.getPreferenceStore().setValue(IWorkbenchPreferenceConstants.SHOW_MEMORY_MONITOR, true); + //PlatformUI.getPreferenceStore().setValue(IWorkbenchPreferenceConstants.SHOW_MEMORY_MONITOR, true); configurer.setShowProgressIndicator(true); //configurer.setShowFastViewBars(false); configurer.setShowPerspectiveBar(true); - - configurer.setTitle("Version - "+Version.getClientFullVersion() + "(" + TimeZone.getDefault().getDisplayName() + ")"); + + WorkspaceManager wm = WorkspaceManager.getInstance(); + String wsName = wm.getDisplayName(wm.getCurrentWorkspacePath()); + configurer.setTitle("Version - "+Version.getClientFullVersion() + " [" + wsName + "] (" + TimeZone.getDefault().getDisplayName() + ")"); } - + @SuppressWarnings("restriction") public void postWindowOpen() { - super.postWindowOpen(); - removeUnwantedMenus(); + super.postWindowOpen(); + removeUnwantedMenus(); + checkGitHubSettings(); } - @SuppressWarnings("restriction") - private void removeUnwantedMenus() { - IWorkbenchWindow window = getWindowConfigurer().getWindow(); - if (window instanceof WorkbenchWindow) { - MenuManager menuManager = ((WorkbenchWindow) window).getMenuManager(); - String[] idsToRemove = { - "org.eclipse.search.menu", - "org.eclipse.ui.run" - }; - for (String id : idsToRemove) { - IContributionItem item = menuManager.find(id); - if (item != null) { - menuManager.remove(item); - } - } - // Also remove by label for any remaining items - for (IContributionItem item : menuManager.getItems()) { - if (item instanceof MenuManager) { - String label = ((MenuManager) item).getMenuText(); - if (label != null && (label.equals("Search") || label.equals("Run") - || label.equals("&Search") || label.equals("&Run"))) { - menuManager.remove(item); - } - } - } - menuManager.update(true); - } - } + @SuppressWarnings("restriction") + private void removeUnwantedMenus() { + IWorkbenchWindow window = getWindowConfigurer().getWindow(); + if (window instanceof WorkbenchWindow) { + MenuManager menuManager = ((WorkbenchWindow) window).getMenuManager(); + String[] idsToRemove = { + "org.eclipse.search.menu", + "org.eclipse.ui.run" + }; + for (String id : idsToRemove) { + IContributionItem item = menuManager.find(id); + if (item != null) { + menuManager.remove(item); + } + } + // Also remove by label for any remaining items + for (IContributionItem item : menuManager.getItems()) { + if (item instanceof MenuManager) { + String label = ((MenuManager) item).getMenuText(); + if (label != null && (label.equals("Search") || label.equals("Run") + || label.equals("&Search") || label.equals("&Run"))) { + menuManager.remove(item); + } + } + } + menuManager.update(true); + } + } - @SuppressWarnings("restriction") - private void removeUnwantedActionSets() { - ActionSetRegistry reg = WorkbenchPlugin.getDefault().getActionSetRegistry(); - IActionSetDescriptor[] actionSets = reg.getActionSets(); - for (IActionSetDescriptor actionSet : actionSets) { - String id = actionSet.getId(); - if (id.startsWith("org.eclipse.search") || id.startsWith("org.eclipse.ui.run") - || id.startsWith("org.eclipse.debug") || id.startsWith("org.eclipse.ui.externaltools")) { - IExtension ext = actionSet.getConfigurationElement().getDeclaringExtension(); - reg.removeExtension(ext, new Object[]{actionSet}); - } - } - } + @SuppressWarnings("restriction") + private void removeUnwantedActionSets() { + ActionSetRegistry reg = WorkbenchPlugin.getDefault().getActionSetRegistry(); + IActionSetDescriptor[] actionSets = reg.getActionSets(); + for (IActionSetDescriptor actionSet : actionSets) { + String id = actionSet.getId(); + if (id.startsWith("org.eclipse.search") || id.startsWith("org.eclipse.ui.run") + || id.startsWith("org.eclipse.debug") || id.startsWith("org.eclipse.ui.externaltools")) { + IExtension ext = actionSet.getConfigurationElement().getDeclaringExtension(); + reg.removeExtension(ext, new Object[]{actionSet}); + } + } + checkGitHubSettings(); + } + + private void checkGitHubSettings() { + ExUtil.asyncRun(() -> { + try { + ThreadUtil.sleep(3000); + if (ImportFromGitHubDialog.hasNewSettings()) { + Display.getDefault().asyncExec(() -> { + Shell shell = PlatformUI.getWorkbench().getActiveWorkbenchWindow().getShell(); + MessageDialog dialog = new MessageDialog( + shell, + "New Settings Available", + null, + "New workspace settings are available on GitHub.\nWould you like to import them now?", + MessageDialog.INFORMATION, + new String[] { "Import Now", "Remind Me Later", "Don't Ask for 30 Days" }, + 0); + int result = dialog.open(); + if (result == 0) { + new ImportFromGitHubDialog(shell).open(); + } else if (result == 2) { + ImportFromGitHubDialog.snooze30Days(); + } + }); + } + } catch (Exception e) { + // ignore + } + }); + } public void dispose() { super.dispose(); diff --git a/scouter.client/src/scouter/client/PerspectiveService.java b/scouter.client/src/scouter/client/PerspectiveService.java index 165e1a9c7..c68764294 100644 --- a/scouter.client/src/scouter/client/PerspectiveService.java +++ b/scouter.client/src/scouter/client/PerspectiveService.java @@ -1,5 +1,5 @@ /* - * Copyright 2015 the original author or authors. + * Copyright 2015 the original author or authors. * @https://github.com/scouter-project/scouter * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -59,33 +59,35 @@ public void createInitialLayout(IPageLayout layout) { agentLayout.addPlaceholder(WorkspaceExplorer.ID); agentLayout.addPlaceholder(GroupNavigationView.ID); agentLayout.addView(ObjectNavigationView.ID); - layout.getViewLayout(ObjectNavigationView.ID).setCloseable(false); - - IFolderLayout eqLayout = layout.createFolder(IConstants.LAYOUT_WASSERVICE_EQ, IPageLayout.BOTTOM, 0.5f, IConstants.LAYOUT_WASSERVICE_OBJECT_NAVIGATION); - eqLayout.addPlaceholder(EQView.ID + ":*"); - eqLayout.addView(EQView.ID + ":" + serverId +"&"+ objType); // 1 + layout.getViewLayout(ObjectNavigationView.ID).setCloseable(false); - IFolderLayout cpuLayout = layout.createFolder(IConstants.LAYOUT_WASSERVICE_CPU, IPageLayout.BOTTOM, 0.5f, IConstants.LAYOUT_WASSERVICE_EQ); - cpuLayout.addView(CounterRealTimeAllView.ID + ":" + serverId + "&" + host + "&" + CounterConstants.HOST_CPU); - IFolderLayout alertLayout = layout.createFolder(IConstants.LAYOUT_WASSERVICE_ALERT, IPageLayout.BOTTOM, 0.5f, IConstants.LAYOUT_WASSERVICE_OBJECT_NAVIGATION); alertLayout.addPlaceholder(AlertView.ID + ":*"); alertLayout.addView(AlertView.ID); - - IFolderLayout upResLayout = layout.createFolder(IConstants.LAYOUT_WASSERVICE_LEFT_TOP, IPageLayout.LEFT, 0.3f, editorArea); - upResLayout.addView(CounterRealTimeAllView.ID + ":" + serverId + "&" + objType + "&" + CounterConstants.WAS_RECENT_USER); - - IFolderLayout midResLayout = layout.createFolder(IConstants.LAYOUT_WASSERVICE_LEFT_MID1, IPageLayout.BOTTOM, 0.25f, IConstants.LAYOUT_WASSERVICE_LEFT_TOP); - midResLayout.addView(CounterRealTimeTotalView.ID + ":" + serverId + "&" + objType + "&" + CounterConstants.WAS_TPS); - IFolderLayout mid2ResLayout = layout.createFolder(IConstants.LAYOUT_WASSERVICE_LEFT_MID2, IPageLayout.BOTTOM, 0.33f, IConstants.LAYOUT_WASSERVICE_LEFT_MID1); - mid2ResLayout.addView(CounterRealTimeAllView.ID + ":" + serverId + "&" + objType + "&" + CounterConstants.WAS_ELAPSED_TIME); - - IFolderLayout downResLayout = layout.createFolder(IConstants.LAYOUT_WASSERVICE_LEFT_BOTTOM, IPageLayout.BOTTOM, 0.5f, IConstants.LAYOUT_WASSERVICE_LEFT_MID2); - downResLayout.addView(CounterRealTimeAllView.ID + ":" + serverId + "&" + objType + "&" + CounterConstants.JAVA_HEAP_USED); + if (server != null) { + IFolderLayout eqLayout = layout.createFolder(IConstants.LAYOUT_WASSERVICE_EQ, IPageLayout.BOTTOM, 0.5f, IConstants.LAYOUT_WASSERVICE_OBJECT_NAVIGATION); + eqLayout.addPlaceholder(EQView.ID + ":*"); + eqLayout.addView(EQView.ID + ":" + serverId +"&"+ objType); + + IFolderLayout cpuLayout = layout.createFolder(IConstants.LAYOUT_WASSERVICE_CPU, IPageLayout.BOTTOM, 0.5f, IConstants.LAYOUT_WASSERVICE_EQ); + cpuLayout.addView(CounterRealTimeAllView.ID + ":" + serverId + "&" + host + "&" + CounterConstants.HOST_CPU); - IFolderLayout xlogTopLayout = layout.createFolder(IConstants.LAYOUT_WASSERVICE_CENTER_TOP, IPageLayout.LEFT, 1f, editorArea); - xlogTopLayout.addView(XLogRealTimeView.ID + ":" + serverId + "&" + objType); + IFolderLayout upResLayout = layout.createFolder(IConstants.LAYOUT_WASSERVICE_LEFT_TOP, IPageLayout.LEFT, 0.3f, editorArea); + upResLayout.addView(CounterRealTimeAllView.ID + ":" + serverId + "&" + objType + "&" + CounterConstants.WAS_RECENT_USER); + + IFolderLayout midResLayout = layout.createFolder(IConstants.LAYOUT_WASSERVICE_LEFT_MID1, IPageLayout.BOTTOM, 0.25f, IConstants.LAYOUT_WASSERVICE_LEFT_TOP); + midResLayout.addView(CounterRealTimeTotalView.ID + ":" + serverId + "&" + objType + "&" + CounterConstants.WAS_TPS); + + IFolderLayout mid2ResLayout = layout.createFolder(IConstants.LAYOUT_WASSERVICE_LEFT_MID2, IPageLayout.BOTTOM, 0.33f, IConstants.LAYOUT_WASSERVICE_LEFT_MID1); + mid2ResLayout.addView(CounterRealTimeAllView.ID + ":" + serverId + "&" + objType + "&" + CounterConstants.WAS_ELAPSED_TIME); + + IFolderLayout downResLayout = layout.createFolder(IConstants.LAYOUT_WASSERVICE_LEFT_BOTTOM, IPageLayout.BOTTOM, 0.5f, IConstants.LAYOUT_WASSERVICE_LEFT_MID2); + downResLayout.addView(CounterRealTimeAllView.ID + ":" + serverId + "&" + objType + "&" + CounterConstants.JAVA_HEAP_USED); + + IFolderLayout xlogTopLayout = layout.createFolder(IConstants.LAYOUT_WASSERVICE_CENTER_TOP, IPageLayout.LEFT, 1f, editorArea); + xlogTopLayout.addView(XLogRealTimeView.ID + ":" + serverId + "&" + objType); + } layout.addPerspectiveShortcut(getId()); diff --git a/scouter.client/src/scouter/client/actions/ImportWorkspaceFromFileAction.java b/scouter.client/src/scouter/client/actions/ImportWorkspaceFromFileAction.java new file mode 100644 index 000000000..f8ec7fc3a --- /dev/null +++ b/scouter.client/src/scouter/client/actions/ImportWorkspaceFromFileAction.java @@ -0,0 +1,83 @@ +/* + * Copyright 2015 the original author or authors. + * @https://github.com/scouter-project/scouter + * + * 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 scouter.client.actions; + + +import org.eclipse.core.runtime.Platform; +import org.eclipse.jface.action.Action; +import org.eclipse.jface.dialogs.MessageDialog; +import org.eclipse.swt.SWT; +import org.eclipse.swt.graphics.Image; +import org.eclipse.swt.widgets.FileDialog; +import org.eclipse.ui.IWorkbenchWindow; +import org.eclipse.ui.PlatformUI; +import scouter.client.util.ClientFileUtil; +import scouter.client.util.ExUtil; +import scouter.client.util.ImageUtil; +import scouter.client.util.ZipUtil; +import scouter.util.FileUtil; +import scouter.util.StringUtil; + +import java.io.File; + +public class ImportWorkspaceFromFileAction extends Action { + public final static String ID = ImportWorkspaceFromFileAction.class.getName(); + + private final IWorkbenchWindow window; + + public ImportWorkspaceFromFileAction(IWorkbenchWindow window, String label, Image image) { + this.window = window; + setText(label); + setId(ID); + setActionDefinitionId(ID); + setImageDescriptor(ImageUtil.getImageDescriptor(image)); + } + + public void run() { + if (window != null) { + FileDialog dialog = new FileDialog(window.getShell(), SWT.OPEN); + + dialog.setFilterNames(new String[] {"scouter export zip files", "zip Files (*.zip)"}); + dialog.setFilterExtensions(new String[] {"*.zip"}); + + String importFileName = dialog.open(); + if (StringUtil.isEmpty(importFileName)) { + return; + } + + String workspaceRootName = Platform.getInstanceLocation().getURL().getFile(); + String importWorkingDirName = workspaceRootName + "/import-working"; + + ClientFileUtil.deleteDirectory(new File(importWorkingDirName)); + FileUtil.mkdirs(importWorkingDirName); + try { + ZipUtil.decompress(importFileName, importWorkingDirName); + } catch (Throwable throwable) { + throwable.printStackTrace(); + } + + String message = "Import completed.\nRestarting..."; + MessageDialog.openInformation(window.getShell(), "Info", message); + ExUtil.exec(new Runnable() { + public void run() { + PlatformUI.getWorkbench().restart(); + } + }); + } + } +} diff --git a/scouter.client/src/scouter/client/actions/ImportWorkspaceFromGitHubAction.java b/scouter.client/src/scouter/client/actions/ImportWorkspaceFromGitHubAction.java new file mode 100644 index 000000000..2d9c1d21b --- /dev/null +++ b/scouter.client/src/scouter/client/actions/ImportWorkspaceFromGitHubAction.java @@ -0,0 +1,45 @@ +/* + * Copyright 2015 the original author or authors. + * @https://github.com/scouter-project/scouter + * + * 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 scouter.client.actions; + +import org.eclipse.jface.action.Action; +import org.eclipse.swt.graphics.Image; +import org.eclipse.ui.IWorkbenchWindow; +import scouter.client.popup.ImportFromGitHubDialog; +import scouter.client.util.ImageUtil; + +public class ImportWorkspaceFromGitHubAction extends Action { + public final static String ID = ImportWorkspaceFromGitHubAction.class.getName(); + + private final IWorkbenchWindow window; + + public ImportWorkspaceFromGitHubAction(IWorkbenchWindow window, String label, Image image) { + this.window = window; + setText(label); + setId(ID); + setActionDefinitionId(ID); + setImageDescriptor(ImageUtil.getImageDescriptor(image)); + } + + public void run() { + if (window != null) { + ImportFromGitHubDialog dialog = new ImportFromGitHubDialog(window.getShell()); + dialog.open(); + } + } +} diff --git a/scouter.client/src/scouter/client/actions/SwitchWorkspaceAction.java b/scouter.client/src/scouter/client/actions/SwitchWorkspaceAction.java new file mode 100644 index 000000000..0224dc2ee --- /dev/null +++ b/scouter.client/src/scouter/client/actions/SwitchWorkspaceAction.java @@ -0,0 +1,95 @@ +/* + * Copyright 2015 the original author or authors. + * @https://github.com/scouter-project/scouter + * + * 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 scouter.client.actions; + +import org.eclipse.jface.action.Action; +import org.eclipse.jface.window.Window; +import org.eclipse.ui.IWorkbenchWindow; +import org.eclipse.ui.PlatformUI; + +import scouter.client.Images; +import scouter.client.util.ExUtil; +import scouter.client.util.ImageUtil; +import scouter.client.workspace.SwitchWorkspaceDialog; + +public class SwitchWorkspaceAction extends Action { + public final static String ID = SwitchWorkspaceAction.class.getName(); + + private final IWorkbenchWindow window; + + public SwitchWorkspaceAction(IWorkbenchWindow window, String label) { + this.window = window; + setText(label); + setId(ID); + setActionDefinitionId(ID); + setImageDescriptor(ImageUtil.getImageDescriptor(Images.refresh)); + } + + public void run() { + if (window == null) return; + + SwitchWorkspaceDialog dialog = new SwitchWorkspaceDialog(window.getShell()); + if (dialog.open() != Window.OK) return; + + String selectedPath = dialog.getSelectedPath(); + if (selectedPath == null || selectedPath.isEmpty()) return; + + String commandLine = buildCommandLine(selectedPath); + System.setProperty("eclipse.exitdata", commandLine); + System.setProperty("scouter.workspace.switch", "true"); + ExUtil.exec(new Runnable() { + public void run() { + PlatformUI.getWorkbench().restart(); + } + }); + } + + private String buildCommandLine(String newWorkspacePath) { + String property = System.getProperty("eclipse.commands"); + if (property == null) { + return "-data\n" + newWorkspacePath + "\n"; + } + + StringBuilder result = new StringBuilder(); + String[] lines = property.split("\n"); + boolean skipNext = false; + boolean dataFound = false; + + for (String line : lines) { + if (skipNext) { + skipNext = false; + continue; + } + if ("-data".equals(line.trim())) { + result.append("-data\n"); + result.append(newWorkspacePath).append("\n"); + skipNext = true; + dataFound = true; + } else { + result.append(line).append("\n"); + } + } + + if (!dataFound) { + result.append("-data\n"); + result.append(newWorkspacePath).append("\n"); + } + + return result.toString(); + } +} diff --git a/scouter.client/src/scouter/client/counter/views/CounterAllPairPainter.java b/scouter.client/src/scouter/client/counter/views/CounterAllPairPainter.java index 924967543..668c477c5 100644 --- a/scouter.client/src/scouter/client/counter/views/CounterAllPairPainter.java +++ b/scouter.client/src/scouter/client/counter/views/CounterAllPairPainter.java @@ -57,7 +57,7 @@ public void createPartControl(Composite parent) { layout.marginHeight = 5; layout.marginWidth = 5; parent.setLayout(layout); - parent.setBackground(ColorUtil.getInstance().getColor(SWT.COLOR_WHITE)); + parent.setBackground(ColorUtil.getChartBackground()); parent.setBackgroundMode(SWT.INHERIT_FORCE); canvas = new FigureCanvas(parent); @@ -89,18 +89,25 @@ public void controlMoved(ControlEvent e) { xyGraph.setShowLegend(false); xyGraph.setShowTitle(false); canvas.setContents(xyGraph); - + xyGraph.primaryXAxis.setDateEnabled(true); xyGraph.primaryXAxis.setShowMajorGrid(true); - + xyGraph.primaryYAxis.setAutoScale(true); xyGraph.primaryYAxis.setShowMajorGrid(true); - + xyGraph.primaryXAxis.setTitle(""); xyGraph.primaryYAxis.setTitle(""); - + xyGraph.primaryXAxis.setFormatPattern("HH:mm:ss"); xyGraph.primaryYAxis.setFormatPattern("#,##0"); + + // Apply dark mode colors + xyGraph.getPlotArea().setBackgroundColor(ColorUtil.getChartBackground()); + xyGraph.primaryXAxis.setForegroundColor(ColorUtil.getChartForeground()); + xyGraph.primaryYAxis.setForegroundColor(ColorUtil.getChartForeground()); + xyGraph.primaryXAxis.setMajorGridColor(ColorUtil.getAxisGridColor()); + xyGraph.primaryYAxis.setMajorGridColor(ColorUtil.getAxisGridColor()); xyGraph.primaryYAxis.addMouseListener(new RangeMouseListener(getViewSite().getShell(), xyGraph.primaryYAxis)); diff --git a/scouter.client/src/scouter/client/model/AgentColorManager.java b/scouter.client/src/scouter/client/model/AgentColorManager.java index bd6bfdfa8..ddfc5f426 100644 --- a/scouter.client/src/scouter/client/model/AgentColorManager.java +++ b/scouter.client/src/scouter/client/model/AgentColorManager.java @@ -1,5 +1,5 @@ /* - * Copyright 2015 the original author or authors. + * Copyright 2015 the original author or authors. * @https://github.com/scouter-project/scouter * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -54,7 +54,8 @@ public Color assignColor(String objType, int objHash) { assignedIndex.put(objType, 0); } int index = assignedIndex.get(objType); - color = searchAvaliableColor(ColorUtil.default_rgb_map[index % ColorUtil.default_rgb_map.length]); + RGB[] rgbMap = ColorUtil.getDefaultRgbMap(); + color = searchAvaliableColor(rgbMap[index % rgbMap.length]); assignedColor.put(objHash, color); if (index >= ColorUtil.default_rgb_map.length - 1) { assignedIndex.put(objType, 0); diff --git a/scouter.client/src/scouter/client/popup/ImportFromGitHubDialog.java b/scouter.client/src/scouter/client/popup/ImportFromGitHubDialog.java new file mode 100644 index 000000000..01102f10b --- /dev/null +++ b/scouter.client/src/scouter/client/popup/ImportFromGitHubDialog.java @@ -0,0 +1,1165 @@ +/* + * Copyright 2015 the original author or authors. + * @https://github.com/scouter-project/scouter + * + * 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 scouter.client.popup; + +import org.apache.http.HttpEntity; +import org.apache.http.HttpResponse; +import org.apache.http.client.HttpClient; +import org.apache.http.client.config.RequestConfig; +import org.apache.http.client.methods.HttpGet; +import org.apache.http.impl.client.HttpClientBuilder; +import org.apache.http.impl.client.LaxRedirectStrategy; +import org.apache.http.util.EntityUtils; +import org.eclipse.core.runtime.Platform; +import org.eclipse.jface.dialogs.MessageDialog; +import org.eclipse.swt.SWT; +import org.eclipse.swt.events.SelectionAdapter; +import org.eclipse.swt.events.SelectionEvent; +import org.eclipse.swt.layout.GridData; +import org.eclipse.swt.layout.GridLayout; +import org.eclipse.swt.widgets.*; +import org.eclipse.ui.PlatformUI; +import scouter.client.util.ClientFileUtil; +import scouter.client.util.ExUtil; +import scouter.client.util.ZipUtil; +import scouter.util.FileUtil; + +import java.io.*; +import java.net.URI; +import java.util.ArrayList; +import java.util.LinkedList; +import java.util.Properties; +import java.util.concurrent.TimeUnit; + +public class ImportFromGitHubDialog { + + private static final String HISTORY_FILE = "github_import_history.properties"; + private static final String KEY_REPO_URLS = "repoUrls"; + private static final String KEY_BRANCHES = "branches"; + private static final String KEY_PATHS = "paths"; + private static final String KEY_LAST_REPO_URL = "lastRepoUrl"; + private static final String KEY_LAST_BRANCH = "lastBranch"; + private static final String KEY_LAST_PATH = "lastPath"; + private static final String KEY_LAST_TOKEN = "lastToken"; + private static final String KEY_LAST_IMPORT_TIME = "lastImportTime"; + private static final String KEY_SNOOZE_UNTIL = "snoozeUntil"; + private static final int MAX_HISTORY = 10; + + private final Shell parentShell; + private Shell dialog; + + private Combo repoUrlCombo; + private Combo branchCombo; + private Combo pathCombo; + private Text tokenText; + private Table fileTable; + private Button importButton; + + private java.util.List fileEntries = new ArrayList<>(); + + public ImportFromGitHubDialog(Shell parentShell) { + this.parentShell = parentShell; + } + + public void open() { + dialog = new Shell(parentShell, SWT.DIALOG_TRIM | SWT.APPLICATION_MODAL | SWT.RESIZE); + dialog.setText("Import from GitHub"); + dialog.setLayout(new GridLayout(3, false)); + dialog.setSize(600, 450); + + // Repository URL + Label repoUrlLabel = new Label(dialog, SWT.NONE); + repoUrlLabel.setText("Repository URL:"); + repoUrlCombo = new Combo(dialog, SWT.DROP_DOWN); + repoUrlCombo.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, true, false, 2, 1)); + repoUrlCombo.setToolTipText("https://github.com/owner/repo"); + + // Branch + Label branchLabel = new Label(dialog, SWT.NONE); + branchLabel.setText("Branch:"); + branchCombo = new Combo(dialog, SWT.DROP_DOWN); + branchCombo.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, true, false, 2, 1)); + branchCombo.setText("main"); + + // Path + Label pathLabel = new Label(dialog, SWT.NONE); + pathLabel.setText("Path:"); + pathCombo = new Combo(dialog, SWT.DROP_DOWN); + pathCombo.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, true, false, 2, 1)); + pathCombo.setText("/"); + + // Token (optional) + Label tokenLabel = new Label(dialog, SWT.NONE); + tokenLabel.setText("Token:"); + tokenText = new Text(dialog, SWT.BORDER | SWT.PASSWORD); + tokenText.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, true, false, 1, 1)); + tokenText.setMessage("auto-detect from git credential"); + + // Load button + Button loadButton = new Button(dialog, SWT.PUSH); + loadButton.setText("Load"); + loadButton.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent e) { + loadFileList(); + } + }); + + // File table + fileTable = new Table(dialog, SWT.BORDER | SWT.FULL_SELECTION | SWT.SINGLE); + GridData tableData = new GridData(SWT.FILL, SWT.FILL, true, true, 3, 1); + fileTable.setLayoutData(tableData); + fileTable.setHeaderVisible(true); + fileTable.setLinesVisible(true); + + TableColumn nameColumn = new TableColumn(fileTable, SWT.NONE); + nameColumn.setText("File Name"); + nameColumn.setWidth(350); + + TableColumn sizeColumn = new TableColumn(fileTable, SWT.NONE); + sizeColumn.setText("Size"); + sizeColumn.setWidth(100); + + fileTable.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent e) { + importButton.setEnabled(fileTable.getSelectionIndex() >= 0); + } + }); + + // Bottom buttons + Composite buttonComposite = new Composite(dialog, SWT.NONE); + buttonComposite.setLayoutData(new GridData(SWT.RIGHT, SWT.CENTER, true, false, 3, 1)); + buttonComposite.setLayout(new GridLayout(2, true)); + + importButton = new Button(buttonComposite, SWT.PUSH); + importButton.setText("Import"); + importButton.setEnabled(false); + importButton.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, true, false)); + importButton.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent e) { + importSelectedFile(); + } + }); + + Button cancelButton = new Button(buttonComposite, SWT.PUSH); + cancelButton.setText("Cancel"); + cancelButton.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, true, false)); + cancelButton.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent e) { + dialog.close(); + } + }); + + loadHistory(); + + dialog.open(); + } + + private void loadFileList() { + String repoUrl = repoUrlCombo.getText().trim(); + String branch = branchCombo.getText().trim(); + String path = pathCombo.getText().trim(); + + if (repoUrl.isEmpty()) { + MessageDialog.openWarning(dialog, "Warning", "Please enter Repository URL."); + return; + } + + ParsedRepo parsed = parseRepoUrl(repoUrl); + if (parsed == null) { + MessageDialog.openWarning(dialog, "Warning", + "Invalid Repository URL.\nExpected format: https://github.com/owner/repo"); + return; + } + + if (branch.isEmpty()) { + branch = "main"; + } + if (path.isEmpty() || path.equals("/")) { + path = ""; + } + if (path.startsWith("/")) { + path = path.substring(1); + } + + String apiUrl = parsed.apiBase + "/repos/" + parsed.ownerRepo + "/contents/" + path + "?ref=" + branch; + + fileTable.removeAll(); + fileEntries.clear(); + importButton.setEnabled(false); + + final String finalBranch = branch; + final String finalPath = path; + + try { + String resolvedToken = resolveGitCredential(parsed.host); + + HttpResponse response = executeGitHubApiGet(apiUrl, resolvedToken); + int statusCode = response.getStatusLine().getStatusCode(); + + // If 401 with "token" format, retry with "Bearer" format + if (statusCode == 401 && resolvedToken != null) { + EntityUtils.consumeQuietly(response.getEntity()); + response = executeGitHubApiGet(apiUrl, "Bearer:" + resolvedToken); + statusCode = response.getStatusLine().getStatusCode(); + } + + HttpEntity entity = response.getEntity(); + String json = EntityUtils.toString(entity, "UTF-8"); + + if (statusCode != 200) { + MessageDialog.openError(dialog, "Error", + "GitHub API returned status " + statusCode + "." + + "\nURL: " + apiUrl + + "\nToken: " + (resolvedToken != null ? maskToken(resolvedToken) + " (len=" + resolvedToken.length() + ")" : "(none)") + + "\nResponse: " + (json.length() > 300 ? json.substring(0, 300) + "..." : json)); + return; + } + + java.util.List entries = parseGitHubContentsResponse(json); + for (GitHubFileEntry entry : entries) { + if (entry.name.toLowerCase().endsWith(".zip") && "file".equals(entry.type)) { + entry.host = parsed.host; + entry.apiBase = parsed.apiBase; + entry.ownerRepo = parsed.ownerRepo; + entry.branch = finalBranch; + entry.filePath = (finalPath.isEmpty() ? "" : finalPath + "/") + entry.name; + fileEntries.add(entry); + TableItem item = new TableItem(fileTable, SWT.NONE); + item.setText(0, entry.name); + item.setText(1, formatSize(entry.size)); + } + } + + if (fileEntries.isEmpty()) { + MessageDialog.openInformation(dialog, "Info", "No .zip files found in the specified path."); + } + + saveHistory(repoUrl, finalBranch, pathCombo.getText().trim()); + + } catch (Exception e) { + e.printStackTrace(); + MessageDialog.openError(dialog, "Error", "Failed to load file list: " + e.getMessage()); + } + } + + private void importSelectedFile() { + int index = fileTable.getSelectionIndex(); + if (index < 0 || index >= fileEntries.size()) { + return; + } + + GitHubFileEntry entry = fileEntries.get(index); + + String downloadUrl = entry.downloadUrl; + String acceptHeader = null; + if (downloadUrl == null || downloadUrl.isEmpty()) { + downloadUrl = entry.apiBase + "/repos/" + entry.ownerRepo + "/contents/" + entry.filePath + "?ref=" + entry.branch; + acceptHeader = "application/vnd.github.v3.raw"; + } + + try { + HttpClient httpClient = HttpClientBuilder.create().build(); + HttpGet httpGet = new HttpGet(downloadUrl); + httpGet.addHeader("User-Agent", "Scouter-Client"); + if (acceptHeader != null) { + httpGet.addHeader("Accept", acceptHeader); + } + addAuthHeader(httpGet, entry.host); + + HttpResponse response = httpClient.execute(httpGet); + int statusCode = response.getStatusLine().getStatusCode(); + + if (statusCode != 200) { + MessageDialog.openError(dialog, "Error", "Failed to download file. HTTP status: " + statusCode); + return; + } + + String workspaceRootName = Platform.getInstanceLocation().getURL().getFile(); + String tempDir = workspaceRootName + "/import-temp"; + ClientFileUtil.deleteDirectory(new File(tempDir)); + FileUtil.mkdirs(tempDir); + + String tempFilePath = tempDir + "/" + entry.name; + HttpEntity downloadEntity = response.getEntity(); + InputStream inputStream = downloadEntity.getContent(); + FileOutputStream fos = new FileOutputStream(tempFilePath); + try { + byte[] buffer = new byte[4096]; + int bytesRead; + while ((bytesRead = inputStream.read(buffer)) != -1) { + fos.write(buffer, 0, bytesRead); + } + } finally { + fos.close(); + inputStream.close(); + } + + String importWorkingDirName = workspaceRootName + "/import-working"; + ClientFileUtil.deleteDirectory(new File(importWorkingDirName)); + FileUtil.mkdirs(importWorkingDirName); + try { + ZipUtil.decompress(tempFilePath, importWorkingDirName); + } catch (Throwable t) { + t.printStackTrace(); + } + + ClientFileUtil.deleteDirectory(new File(tempDir)); + + saveLastImportTime(); + + dialog.close(); + + MessageDialog.openInformation(parentShell, "Info", "Import completed.\nRestarting..."); + ExUtil.exec(new Runnable() { + public void run() { + PlatformUI.getWorkbench().restart(); + } + }); + + } catch (Exception e) { + e.printStackTrace(); + MessageDialog.openError(dialog, "Error", "Failed to import file: " + e.getMessage()); + } + } + + private ParsedRepo parseRepoUrl(String repoUrl) { + try { + if (!repoUrl.startsWith("http://") && !repoUrl.startsWith("https://")) { + repoUrl = "https://" + repoUrl; + } + URI uri = new URI(repoUrl); + String host = uri.getHost(); + if (host == null) return null; + + String uriPath = uri.getPath(); + if (uriPath == null || uriPath.isEmpty()) return null; + if (uriPath.startsWith("/")) { + uriPath = uriPath.substring(1); + } + if (uriPath.endsWith("/")) { + uriPath = uriPath.substring(0, uriPath.length() - 1); + } + + String[] segments = uriPath.split("/"); + if (segments.length < 2) return null; + + String repo = segments[1]; + if (repo.endsWith(".git")) { + repo = repo.substring(0, repo.length() - 4); + } + String ownerRepo = segments[0] + "/" + repo; + String apiBase; + if ("github.com".equalsIgnoreCase(host)) { + apiBase = "https://api.github.com"; + } else { + apiBase = "https://" + host + "/api/v3"; + } + + ParsedRepo result = new ParsedRepo(); + result.host = host; + result.apiBase = apiBase; + result.ownerRepo = ownerRepo; + return result; + } catch (Exception e) { + return null; + } + } + + private String getHistoryFilePath() { + try { + String workspace = Platform.getInstanceLocation().getURL().getFile(); + return workspace + "/" + HISTORY_FILE; + } catch (Exception e) { + return null; + } + } + + private void loadHistory() { + String filePath = getHistoryFilePath(); + if (filePath == null) return; + + File file = new File(filePath); + if (!file.exists()) return; + + Properties props = new Properties(); + try (FileInputStream fis = new FileInputStream(file)) { + props.load(fis); + } catch (Exception e) { + return; + } + + String repoUrls = props.getProperty(KEY_REPO_URLS, ""); + String branches = props.getProperty(KEY_BRANCHES, ""); + String paths = props.getProperty(KEY_PATHS, ""); + + if (!repoUrls.isEmpty()) { + for (String item : repoUrls.split("\n")) { + if (!item.trim().isEmpty()) { + repoUrlCombo.add(item.trim()); + } + } + } + if (!branches.isEmpty()) { + for (String item : branches.split("\n")) { + if (!item.trim().isEmpty()) { + branchCombo.add(item.trim()); + } + } + } + if (!paths.isEmpty()) { + for (String item : paths.split("\n")) { + if (!item.trim().isEmpty()) { + pathCombo.add(item.trim()); + } + } + } + + // Restore last-used values + String lastRepoUrl = props.getProperty(KEY_LAST_REPO_URL, ""); + String lastBranch = props.getProperty(KEY_LAST_BRANCH, ""); + String lastPath = props.getProperty(KEY_LAST_PATH, ""); + String lastToken = props.getProperty(KEY_LAST_TOKEN, ""); + + if (!lastRepoUrl.isEmpty()) { + repoUrlCombo.setText(lastRepoUrl); + } + if (!lastBranch.isEmpty()) { + branchCombo.setText(lastBranch); + } + if (!lastPath.isEmpty()) { + pathCombo.setText(lastPath); + } + if (!lastToken.isEmpty()) { + tokenText.setText(lastToken); + } + } + + private void saveHistory(String repoUrl, String branch, String path) { + String filePath = getHistoryFilePath(); + if (filePath == null) return; + + String token = tokenText != null ? tokenText.getText().trim() : ""; + + Properties props = new Properties(); + File file = new File(filePath); + if (file.exists()) { + try (FileInputStream fis = new FileInputStream(file)) { + props.load(fis); + } catch (Exception e) { + // ignore + } + } + + String repoUrls = addToHistory(props.getProperty(KEY_REPO_URLS, ""), repoUrl); + String branches = addToHistory(props.getProperty(KEY_BRANCHES, ""), branch); + String paths = addToHistory(props.getProperty(KEY_PATHS, ""), path); + + props.setProperty(KEY_REPO_URLS, repoUrls); + props.setProperty(KEY_BRANCHES, branches); + props.setProperty(KEY_PATHS, paths); + + // Save last-used values + props.setProperty(KEY_LAST_REPO_URL, repoUrl); + props.setProperty(KEY_LAST_BRANCH, branch); + props.setProperty(KEY_LAST_PATH, path); + props.setProperty(KEY_LAST_TOKEN, token); + + try (FileOutputStream fos = new FileOutputStream(file)) { + props.store(fos, "GitHub Import History"); + } catch (Exception e) { + // ignore + } + + refreshComboItems(repoUrlCombo, repoUrls); + refreshComboItems(branchCombo, branches); + refreshComboItems(pathCombo, paths); + } + + private String addToHistory(String existing, String newValue) { + if (newValue == null || newValue.trim().isEmpty()) return existing; + newValue = newValue.trim(); + + LinkedList list = new LinkedList<>(); + if (!existing.isEmpty()) { + for (String item : existing.split("\n")) { + if (!item.trim().isEmpty()) { + list.add(item.trim()); + } + } + } + list.remove(newValue); + list.addFirst(newValue); + while (list.size() > MAX_HISTORY) { + list.removeLast(); + } + + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < list.size(); i++) { + if (i > 0) sb.append("\n"); + sb.append(list.get(i)); + } + return sb.toString(); + } + + private void refreshComboItems(Combo combo, String history) { + String currentText = combo.getText(); + combo.removeAll(); + if (!history.isEmpty()) { + for (String item : history.split("\n")) { + if (!item.trim().isEmpty()) { + combo.add(item.trim()); + } + } + } + combo.setText(currentText); + } + + private java.util.List parseGitHubContentsResponse(String json) { + java.util.List entries = new ArrayList<>(); + json = json.trim(); + if (!json.startsWith("[")) { + return entries; + } + + int i = 1; // skip '[' + while (i < json.length()) { + int objStart = json.indexOf('{', i); + if (objStart < 0) break; + int objEnd = findMatchingBrace(json, objStart); + if (objEnd < 0) break; + + String obj = json.substring(objStart, objEnd + 1); + String name = extractJsonStringValue(obj, "name"); + String type = extractJsonStringValue(obj, "type"); + String downloadUrl = extractJsonStringValue(obj, "download_url"); + long size = extractJsonLongValue(obj, "size"); + + if (name != null && type != null) { + GitHubFileEntry entry = new GitHubFileEntry(); + entry.name = name; + entry.type = type; + entry.downloadUrl = downloadUrl; + entry.size = size; + entries.add(entry); + } + + i = objEnd + 1; + } + + return entries; + } + + private int findMatchingBrace(String json, int start) { + int depth = 0; + boolean inString = false; + boolean escaped = false; + for (int i = start; i < json.length(); i++) { + char c = json.charAt(i); + if (escaped) { + escaped = false; + continue; + } + if (c == '\\') { + escaped = true; + continue; + } + if (c == '"') { + inString = !inString; + continue; + } + if (inString) continue; + if (c == '{') depth++; + else if (c == '}') { + depth--; + if (depth == 0) return i; + } + } + return -1; + } + + private String extractJsonStringValue(String json, String key) { + String searchKey = "\"" + key + "\""; + int keyIndex = json.indexOf(searchKey); + if (keyIndex < 0) return null; + + int colonIndex = json.indexOf(':', keyIndex + searchKey.length()); + if (colonIndex < 0) return null; + + int valueStart = colonIndex + 1; + while (valueStart < json.length() && json.charAt(valueStart) == ' ') { + valueStart++; + } + + if (valueStart >= json.length()) return null; + + if (json.charAt(valueStart) == 'n' && json.startsWith("null", valueStart)) { + return null; + } + + if (json.charAt(valueStart) != '"') return null; + + int valueEnd = valueStart + 1; + boolean escaped = false; + StringBuilder sb = new StringBuilder(); + while (valueEnd < json.length()) { + char c = json.charAt(valueEnd); + if (escaped) { + sb.append(c); + escaped = false; + } else if (c == '\\') { + escaped = true; + } else if (c == '"') { + return sb.toString(); + } else { + sb.append(c); + } + valueEnd++; + } + return null; + } + + private long extractJsonLongValue(String json, String key) { + String searchKey = "\"" + key + "\""; + int keyIndex = json.indexOf(searchKey); + if (keyIndex < 0) return 0; + + int colonIndex = json.indexOf(':', keyIndex + searchKey.length()); + if (colonIndex < 0) return 0; + + int valueStart = colonIndex + 1; + while (valueStart < json.length() && json.charAt(valueStart) == ' ') { + valueStart++; + } + + StringBuilder sb = new StringBuilder(); + while (valueStart < json.length()) { + char c = json.charAt(valueStart); + if (c >= '0' && c <= '9') { + sb.append(c); + } else if (sb.length() > 0) { + break; + } + valueStart++; + } + + if (sb.length() == 0) return 0; + try { + return Long.parseLong(sb.toString()); + } catch (NumberFormatException e) { + return 0; + } + } + + private String formatSize(long bytes) { + if (bytes < 1024) return bytes + " B"; + if (bytes < 1024 * 1024) return String.format("%.1f KB", bytes / 1024.0); + return String.format("%.1f MB", bytes / (1024.0 * 1024.0)); + } + + private static class ParsedRepo { + String host; + String apiBase; + String ownerRepo; + } + + private static class GitHubFileEntry { + String name; + String type; + String downloadUrl; + long size; + String host; + String apiBase; + String ownerRepo; + String branch; + String filePath; + } + + private String resolveGitCredential(String host) { + // 1. Check UI token field + if (tokenText != null) { + String uiToken = tokenText.getText().trim(); + if (!uiToken.isEmpty()) { + return uiToken; + } + } + + // 2. Try git credential fill (host-specific, works for both github.com and Enterprise) + String gitPath = findGitPath(); + if (gitPath != null) { + try { + ProcessBuilder pb = new ProcessBuilder(gitPath, "credential", "fill"); + pb.redirectErrorStream(false); + Process process = pb.start(); + + OutputStream os = process.getOutputStream(); + os.write(("protocol=https\nhost=" + host + "\n\n").getBytes("UTF-8")); + os.flush(); + os.close(); + + BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream(), "UTF-8")); + String password = null; + String line; + while ((line = reader.readLine()) != null) { + if (line.startsWith("password=")) { + password = line.substring("password=".length()); + } + } + reader.close(); + + boolean finished = process.waitFor(5, TimeUnit.SECONDS); + if (!finished) { + process.destroyForcibly(); + } else if (password != null && !password.trim().isEmpty()) { + return password.trim(); + } + } catch (Exception e) { + // git credential not available + } + } + + // 3. Fallback: environment variables (only for github.com, as they are not host-specific) + if ("github.com".equalsIgnoreCase(host)) { + String envToken = System.getenv("GH_TOKEN"); + if (envToken == null || envToken.isEmpty()) { + envToken = System.getenv("GITHUB_TOKEN"); + } + if (envToken != null && !envToken.isEmpty()) { + return envToken; + } + } + + return null; + } + + private String findGitPath() { + String[] candidates = { + "/usr/bin/git", + "/usr/local/bin/git", + "/opt/homebrew/bin/git", + "/opt/local/bin/git", + "git" + }; + for (String candidate : candidates) { + try { + File f = new File(candidate); + if (f.isAbsolute() && f.canExecute()) { + return candidate; + } + if (!f.isAbsolute()) { + ProcessBuilder pb = new ProcessBuilder(candidate, "--version"); + pb.redirectErrorStream(true); + Process p = pb.start(); + boolean done = p.waitFor(3, TimeUnit.SECONDS); + if (done && p.exitValue() == 0) { + return candidate; + } + if (!done) { + p.destroyForcibly(); + } + } + } catch (Exception e) { + // try next + } + } + return null; + } + + private HttpResponse executeGitHubApiGet(String url, String token) throws Exception { + RequestConfig config = RequestConfig.custom() + .setRedirectsEnabled(false) + .setConnectTimeout(10000) + .setSocketTimeout(30000) + .build(); + HttpClient httpClient = HttpClientBuilder.create() + .setDefaultRequestConfig(config) + .build(); + + HttpGet httpGet = new HttpGet(url); + httpGet.addHeader("Accept", "application/vnd.github.v3+json"); + httpGet.addHeader("User-Agent", "Scouter-Client"); + addAuthToRequest(httpGet, token); + + HttpResponse response = httpClient.execute(httpGet); + int statusCode = response.getStatusLine().getStatusCode(); + + // Follow redirect manually to preserve Authorization header + if (statusCode == 301 || statusCode == 302 || statusCode == 307) { + String redirectUrl = response.getFirstHeader("Location").getValue(); + EntityUtils.consumeQuietly(response.getEntity()); + HttpGet redirectGet = new HttpGet(redirectUrl); + redirectGet.addHeader("Accept", "application/vnd.github.v3+json"); + redirectGet.addHeader("User-Agent", "Scouter-Client"); + addAuthToRequest(redirectGet, token); + response = httpClient.execute(redirectGet); + } + + return response; + } + + private void addAuthToRequest(HttpGet httpGet, String token) { + if (token == null) return; + // "Bearer:xxx" format means use Bearer, otherwise use "token" prefix + if (token.startsWith("Bearer:")) { + httpGet.addHeader("Authorization", "Bearer " + token.substring(7)); + } else { + httpGet.addHeader("Authorization", "token " + token); + } + } + + private void addAuthHeader(HttpGet httpGet, String host) { + String token = resolveGitCredential(host); + if (token != null) { + httpGet.addHeader("Authorization", "token " + token); + } + } + + private String maskToken(String token) { + if (token == null) return "null"; + if (token.length() <= 8) return "***"; + return token.substring(0, 4) + "..." + token.substring(token.length() - 4); + } + + private void saveLastImportTime() { + String filePath = getHistoryFilePath(); + if (filePath == null) return; + + Properties props = new Properties(); + File file = new File(filePath); + if (file.exists()) { + try (FileInputStream fis = new FileInputStream(file)) { + props.load(fis); + } catch (Exception e) { + // ignore + } + } + props.setProperty(KEY_LAST_IMPORT_TIME, String.valueOf(System.currentTimeMillis())); + try (FileOutputStream fos = new FileOutputStream(file)) { + props.store(fos, "GitHub Import History"); + } catch (Exception e) { + // ignore + } + } + + public static void snooze30Days() { + try { + String workspace = Platform.getInstanceLocation().getURL().getFile(); + String filePath = workspace + "/" + HISTORY_FILE; + File file = new File(filePath); + Properties props = new Properties(); + if (file.exists()) { + try (FileInputStream fis = new FileInputStream(file)) { + props.load(fis); + } + } + long until = System.currentTimeMillis() + 30L * 24 * 60 * 60 * 1000; + props.setProperty(KEY_SNOOZE_UNTIL, String.valueOf(until)); + try (FileOutputStream fos = new FileOutputStream(file)) { + props.store(fos, "GitHub Import History"); + } + } catch (Exception e) { + // ignore + } + } + + public static boolean hasNewSettings() { + try { + String workspace = Platform.getInstanceLocation().getURL().getFile(); + String filePath = workspace + "/" + HISTORY_FILE; + File file = new File(filePath); + if (!file.exists()) return false; + + Properties props = new Properties(); + try (FileInputStream fis = new FileInputStream(file)) { + props.load(fis); + } + + // Check snooze + String snoozeUntil = props.getProperty(KEY_SNOOZE_UNTIL, ""); + if (!snoozeUntil.isEmpty()) { + try { + long until = Long.parseLong(snoozeUntil); + if (System.currentTimeMillis() < until) return false; + } catch (NumberFormatException e) { + // ignore + } + } + + String repoUrl = props.getProperty(KEY_LAST_REPO_URL, ""); + String branch = props.getProperty(KEY_LAST_BRANCH, ""); + String path = props.getProperty(KEY_LAST_PATH, ""); + String token = props.getProperty(KEY_LAST_TOKEN, ""); + String lastImportTimeStr = props.getProperty(KEY_LAST_IMPORT_TIME, ""); + + if (repoUrl.isEmpty()) return false; + + long lastImportTime = 0; + if (!lastImportTimeStr.isEmpty()) { + try { + lastImportTime = Long.parseLong(lastImportTimeStr); + } catch (NumberFormatException e) { + // treat as never imported + } + } + + ParsedRepo parsed = parseRepoUrlStatic(repoUrl); + if (parsed == null) return false; + + if (branch.isEmpty()) branch = "main"; + if (path.isEmpty() || path.equals("/")) path = ""; + if (path.startsWith("/")) path = path.substring(1); + + String resolvedToken = token.isEmpty() ? resolveGitCredentialStatic(parsed.host) : token; + + // Use Commits API to get latest commit time for the path + String commitsUrl = parsed.apiBase + "/repos/" + parsed.ownerRepo + + "/commits?sha=" + branch + + "&path=" + (path.isEmpty() ? "/" : path) + + "&per_page=1"; + + RequestConfig config = RequestConfig.custom() + .setRedirectsEnabled(true) + .setConnectTimeout(10000) + .setSocketTimeout(15000) + .build(); + HttpClient httpClient = HttpClientBuilder.create() + .setDefaultRequestConfig(config) + .build(); + HttpGet httpGet = new HttpGet(commitsUrl); + httpGet.addHeader("Accept", "application/vnd.github.v3+json"); + httpGet.addHeader("User-Agent", "Scouter-Client"); + if (resolvedToken != null) { + httpGet.addHeader("Authorization", "token " + resolvedToken); + } + + HttpResponse response = httpClient.execute(httpGet); + if (response.getStatusLine().getStatusCode() != 200) { + EntityUtils.consumeQuietly(response.getEntity()); + return false; + } + + String json = EntityUtils.toString(response.getEntity(), "UTF-8").trim(); + if (!json.startsWith("[")) return false; + + // Extract the latest commit date (ISO 8601) + // Path: [0].commit.committer.date + int objStart = json.indexOf('{'); + if (objStart < 0) return false; + int objEnd = findMatchingBraceStatic(json, objStart); + if (objEnd < 0) return false; + String commitObj = json.substring(objStart, objEnd + 1); + + String dateStr = extractNestedDateFromCommit(commitObj); + if (dateStr == null) return false; + + long commitTime = parseISO8601(dateStr); + if (commitTime <= 0) return false; + + return commitTime > lastImportTime; + + } catch (Exception e) { + // check failed, skip silently + return false; + } + } + + private static String extractNestedDateFromCommit(String json) { + // Find "commit" object, then "committer" inside it, then "date" + String commitKey = "\"commit\""; + int idx = json.indexOf(commitKey); + if (idx < 0) return null; + int braceStart = json.indexOf('{', idx + commitKey.length()); + if (braceStart < 0) return null; + int braceEnd = findMatchingBraceStatic(json, braceStart); + if (braceEnd < 0) return null; + String commitInner = json.substring(braceStart, braceEnd + 1); + + String committerKey = "\"committer\""; + int cIdx = commitInner.indexOf(committerKey); + if (cIdx < 0) return null; + int cBraceStart = commitInner.indexOf('{', cIdx + committerKey.length()); + if (cBraceStart < 0) return null; + int cBraceEnd = findMatchingBraceStatic(commitInner, cBraceStart); + if (cBraceEnd < 0) return null; + String committerObj = commitInner.substring(cBraceStart, cBraceEnd + 1); + + return extractJsonStringValueStatic(committerObj, "date"); + } + + private static long parseISO8601(String dateStr) { + // Parse "2025-01-15T10:30:00Z" format + try { + dateStr = dateStr.trim(); + if (dateStr.endsWith("Z")) { + dateStr = dateStr.substring(0, dateStr.length() - 1); + } + // Handle timezone offset like +09:00 + int tzIdx = dateStr.lastIndexOf('+'); + if (tzIdx < 10) tzIdx = dateStr.lastIndexOf('-', dateStr.length() - 1); + if (tzIdx > 10) { + dateStr = dateStr.substring(0, tzIdx); + } + String[] parts = dateStr.split("T"); + if (parts.length != 2) return 0; + String[] dateParts = parts[0].split("-"); + String[] timeParts = parts[1].split(":"); + if (dateParts.length != 3 || timeParts.length < 2) return 0; + + java.util.Calendar cal = java.util.Calendar.getInstance(java.util.TimeZone.getTimeZone("UTC")); + cal.set(Integer.parseInt(dateParts[0]), + Integer.parseInt(dateParts[1]) - 1, + Integer.parseInt(dateParts[2]), + Integer.parseInt(timeParts[0]), + Integer.parseInt(timeParts[1]), + timeParts.length > 2 ? Integer.parseInt(timeParts[2].split("\\.")[0]) : 0); + cal.set(java.util.Calendar.MILLISECOND, 0); + return cal.getTimeInMillis(); + } catch (Exception e) { + return 0; + } + } + + private static ParsedRepo parseRepoUrlStatic(String repoUrl) { + try { + if (!repoUrl.startsWith("http://") && !repoUrl.startsWith("https://")) { + repoUrl = "https://" + repoUrl; + } + URI uri = new URI(repoUrl); + String host = uri.getHost(); + if (host == null) return null; + String uriPath = uri.getPath(); + if (uriPath == null || uriPath.isEmpty()) return null; + if (uriPath.startsWith("/")) uriPath = uriPath.substring(1); + if (uriPath.endsWith("/")) uriPath = uriPath.substring(0, uriPath.length() - 1); + String[] segments = uriPath.split("/"); + if (segments.length < 2) return null; + String repo = segments[1]; + if (repo.endsWith(".git")) repo = repo.substring(0, repo.length() - 4); + ParsedRepo result = new ParsedRepo(); + result.host = host; + result.ownerRepo = segments[0] + "/" + repo; + result.apiBase = "github.com".equalsIgnoreCase(host) ? "https://api.github.com" : "https://" + host + "/api/v3"; + return result; + } catch (Exception e) { + return null; + } + } + + private static String resolveGitCredentialStatic(String host) { + String gitPath = findGitPathStatic(); + if (gitPath != null) { + try { + ProcessBuilder pb = new ProcessBuilder(gitPath, "credential", "fill"); + pb.redirectErrorStream(false); + Process process = pb.start(); + OutputStream os = process.getOutputStream(); + os.write(("protocol=https\nhost=" + host + "\n\n").getBytes("UTF-8")); + os.flush(); + os.close(); + BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream(), "UTF-8")); + String password = null; + String line; + while ((line = reader.readLine()) != null) { + if (line.startsWith("password=")) { + password = line.substring("password=".length()); + } + } + reader.close(); + boolean finished = process.waitFor(5, TimeUnit.SECONDS); + if (!finished) { + process.destroyForcibly(); + } else if (password != null && !password.trim().isEmpty()) { + return password.trim(); + } + } catch (Exception e) { + // ignore + } + } + if ("github.com".equalsIgnoreCase(host)) { + String envToken = System.getenv("GH_TOKEN"); + if (envToken == null || envToken.isEmpty()) envToken = System.getenv("GITHUB_TOKEN"); + if (envToken != null && !envToken.isEmpty()) return envToken; + } + return null; + } + + private static String findGitPathStatic() { + String[] candidates = { "/usr/bin/git", "/usr/local/bin/git", "/opt/homebrew/bin/git", "/opt/local/bin/git", "git" }; + for (String candidate : candidates) { + try { + File f = new File(candidate); + if (f.isAbsolute() && f.canExecute()) return candidate; + if (!f.isAbsolute()) { + ProcessBuilder pb = new ProcessBuilder(candidate, "--version"); + pb.redirectErrorStream(true); + Process p = pb.start(); + boolean done = p.waitFor(3, TimeUnit.SECONDS); + if (done && p.exitValue() == 0) return candidate; + if (!done) p.destroyForcibly(); + } + } catch (Exception e) { + // try next + } + } + return null; + } + + private static int findMatchingBraceStatic(String json, int start) { + int depth = 0; + boolean inString = false; + boolean escaped = false; + for (int i = start; i < json.length(); i++) { + char c = json.charAt(i); + if (escaped) { escaped = false; continue; } + if (c == '\\') { escaped = true; continue; } + if (c == '"') { inString = !inString; continue; } + if (inString) continue; + if (c == '{') depth++; + else if (c == '}') { depth--; if (depth == 0) return i; } + } + return -1; + } + + private static String extractJsonStringValueStatic(String json, String key) { + String searchKey = "\"" + key + "\""; + int keyIndex = json.indexOf(searchKey); + if (keyIndex < 0) return null; + int colonIndex = json.indexOf(':', keyIndex + searchKey.length()); + if (colonIndex < 0) return null; + int valueStart = colonIndex + 1; + while (valueStart < json.length() && json.charAt(valueStart) == ' ') valueStart++; + if (valueStart >= json.length()) return null; + if (json.charAt(valueStart) == 'n' && json.startsWith("null", valueStart)) return null; + if (json.charAt(valueStart) != '"') return null; + int valueEnd = valueStart + 1; + boolean escaped = false; + StringBuilder sb = new StringBuilder(); + while (valueEnd < json.length()) { + char c = json.charAt(valueEnd); + if (escaped) { sb.append(c); escaped = false; } + else if (c == '\\') { escaped = true; } + else if (c == '"') { return sb.toString(); } + else { sb.append(c); } + valueEnd++; + } + return null; + } +} diff --git a/scouter.client/src/scouter/client/popup/LoginDialog2.java b/scouter.client/src/scouter/client/popup/LoginDialog2.java index c3a0ba7da..8701ee338 100644 --- a/scouter.client/src/scouter/client/popup/LoginDialog2.java +++ b/scouter.client/src/scouter/client/popup/LoginDialog2.java @@ -49,6 +49,8 @@ public class LoginDialog2 extends Dialog { public static final int TYPE_OPEN_SERVER = 993; public static final int TYPE_EDIT_SERVER = 994; + public static final int SKIP_LOGIN = 99; + Combo addrCombo; Combo socksAddrCombo; @@ -89,7 +91,7 @@ protected Control createDialogArea(Composite parent) { final Group parentGroup = new Group(comp, SWT.NONE); parentGroup.setText("Authentication Info"); parentGroup.setLayout(UIUtil.formLayout(5, 5)); - parentGroup.setLayoutData(UIUtil.formData(null, -1, 0, 0, null, -1, null, -1)); + parentGroup.setLayoutData(UIUtil.formData(0, 0, 0, 0, 100, 0, null, -1)); Label addrLabel = new Label(parentGroup, SWT.RIGHT); addrLabel.setText("Server Address :"); @@ -147,7 +149,7 @@ public void widgetSelected(SelectionEvent e) { final Group socksGroup = new Group(comp, SWT.NONE); socksGroup.setText("SOCKS5"); socksGroup.setLayout(UIUtil.formLayout(5, 5)); - socksGroup.setLayoutData(UIUtil.formData(null, -1, parentGroup, 0, null, -1, null, -1)); + socksGroup.setLayoutData(UIUtil.formData(0, 0, parentGroup, 0, 100, 0, null, -1)); // to use SOCKS5 sock5Check = new Button(socksGroup, SWT.CHECK|SWT.LEFT); @@ -180,7 +182,7 @@ public void widgetSelected(SelectionEvent e) { // console group final Group consoleGroup = new Group(comp, SWT.NONE); consoleGroup.setLayout(UIUtil.formLayout(5, 5)); - consoleGroup.setLayoutData(UIUtil.formData(null, -1, socksGroup, 0, null, -1, null, -1)); + consoleGroup.setLayoutData(UIUtil.formData(0, 0, socksGroup, 0, 100, 0, null, -1)); // connection status console messageList = new List(consoleGroup, SWT.NONE); @@ -263,6 +265,24 @@ public void focusGained(FocusEvent e) { passText.setLayoutData(UIUtil.formData(passLabel, 5, idText, 7, 100, -5, null, -1)); } + @Override + protected void createButtonsForButtonBar(Composite parent) { + super.createButtonsForButtonBar(parent); + if (openType == TYPE_STARTUP) { + createButton(parent, SKIP_LOGIN, "Skip", false); + } + } + + @Override + protected void buttonPressed(int buttonId) { + if (buttonId == SKIP_LOGIN) { + setReturnCode(SKIP_LOGIN); + close(); + return; + } + super.buttonPressed(buttonId); + } + @Override protected void okPressed() { if (loginInToServer(addrCombo.getText(), socksAddrCombo.getText())) { diff --git a/scouter.client/src/scouter/client/preferences/PManager.java b/scouter.client/src/scouter/client/preferences/PManager.java index 5acdb59c1..5a423205e 100644 --- a/scouter.client/src/scouter/client/preferences/PManager.java +++ b/scouter.client/src/scouter/client/preferences/PManager.java @@ -54,6 +54,8 @@ private PManager() { store.setDefault(xLogColumnEnum.getInternalID(), xLogColumnEnum.isDefaultVisible()); } + store.setDefault(PreferenceConstants.P_DARK_MODE, false); + // store.setDefault(PreferenceConstants.P_UPDATE_SERVER_ADDR, PORT_AND_REPOSITORY_FOLDER); // store.setDefault(PreferenceConstants.P_ALERT_DIALOG_TIMEOUT, -1); // store.setDefault(PreferenceConstants.NOTIFY_FATAL_ALERT, true); diff --git a/scouter.client/src/scouter/client/preferences/PreferenceConstants.java b/scouter.client/src/scouter/client/preferences/PreferenceConstants.java index 5d97a82cf..d8fa77666 100644 --- a/scouter.client/src/scouter/client/preferences/PreferenceConstants.java +++ b/scouter.client/src/scouter/client/preferences/PreferenceConstants.java @@ -48,5 +48,7 @@ public class PreferenceConstants { public static final String NOTIFY_WARN_ALERT = "notify_warn_alert"; public static final String NOTIFY_ERROR_ALERT = "notify_error_alert"; public static final String NOTIFY_INFO_ALERT = "notify_info_alert"; - + + public static final String P_DARK_MODE = "dark_mode"; + } diff --git a/scouter.client/src/scouter/client/util/ColorUtil.java b/scouter.client/src/scouter/client/util/ColorUtil.java index f0548e702..fe9ab00ac 100644 --- a/scouter.client/src/scouter/client/util/ColorUtil.java +++ b/scouter.client/src/scouter/client/util/ColorUtil.java @@ -17,9 +17,12 @@ */ package scouter.client.util; +import org.eclipse.swt.SWT; import org.eclipse.swt.graphics.Color; import org.eclipse.swt.graphics.RGB; import org.eclipse.swt.widgets.Display; +import scouter.client.preferences.PManager; +import scouter.client.preferences.PreferenceConstants; import java.util.HashMap; @@ -27,7 +30,7 @@ public class ColorUtil { private static volatile ColorUtil instance; - public static RGB[] default_rgb_map = { + public static RGB[] default_rgb_map = { new RGB(55, 78, 179), new RGB(5, 128, 100), new RGB(55, 178, 180), @@ -36,12 +39,32 @@ public class ColorUtil { new RGB(157, 178, 182), new RGB(105, 128, 203), new RGB(158, 128, 161), - new RGB(1, 2, 222), - new RGB(0, 128, 10), - new RGB(101, 9, 251), - new RGB(41, 121, 138), + new RGB(1, 2, 222), + new RGB(0, 128, 10), + new RGB(101, 9, 251), + new RGB(41, 121, 138), new RGB(11, 50, 249) }; + + public static RGB[] default_rgb_map_dark = { + new RGB(100, 160, 255), + new RGB(50, 210, 170), + new RGB(100, 230, 230), + new RGB(150, 180, 240), + new RGB(200, 170, 220), + new RGB(200, 220, 230), + new RGB(150, 180, 255), + new RGB(210, 170, 210), + new RGB(80, 120, 255), + new RGB(60, 220, 80), + new RGB(170, 100, 255), + new RGB(80, 200, 210), + new RGB(90, 130, 255) + }; + + public static RGB[] getDefaultRgbMap() { + return isDarkMode() ? default_rgb_map_dark : default_rgb_map; + } private HashMap rgb = new HashMap(); @@ -117,7 +140,179 @@ public Color getColor(int id) { Display display = Display.getCurrent(); if (display == null) { display = Display.getDefault(); - } + } return display.getSystemColor(id); } + + // Dark mode support methods + public static boolean isDarkMode() { + boolean prefDarkMode = PManager.getInstance().getBoolean(PreferenceConstants.P_DARK_MODE); + if (prefDarkMode) { + return true; + } + return detectSystemDarkMode(); + } + + private static volatile Boolean darkModeCache = null; + + private static boolean detectSystemDarkMode() { + if (darkModeCache != null) { + return darkModeCache; + } + try { + Display display = Display.getCurrent(); + if (display == null) { + // Non-UI thread: return cached value or false + return darkModeCache != null ? darkModeCache : false; + } + // UI thread: detect and cache + org.eclipse.swt.widgets.Shell[] shells = display.getShells(); + if (shells != null && shells.length > 0) { + Color bg = shells[0].getBackground(); + if (bg != null) { + double luminance = (0.299 * bg.getRed() + 0.587 * bg.getGreen() + 0.114 * bg.getBlue()) / 255.0; + darkModeCache = luminance < 0.5; + return darkModeCache; + } + } + Color sysBg = display.getSystemColor(SWT.COLOR_WIDGET_BACKGROUND); + if (sysBg != null) { + double luminance = (0.299 * sysBg.getRed() + 0.587 * sysBg.getGreen() + 0.114 * sysBg.getBlue()) / 255.0; + darkModeCache = luminance < 0.5; + return darkModeCache; + } + } catch (Exception e) { + } + return false; + } + + // Theme color cache + private static Color chartBackground; + private static Color chartBackgroundDark; + private static Color chartForeground; + private static Color chartForegroundDark; + private static Color chartGridNarrow; + private static Color chartGridNarrowDark; + private static Color chartGridWide; + private static Color chartGridWideDark; + private static Color xlogIgnoreArea; + private static Color xlogIgnoreAreaDark; + private static Color chartBorder; + private static Color chartBorderDark; + private static Color filteredBackground; + private static Color filteredBackgroundDark; + private static Color axisGrid; + private static Color axisGridDark; + + public static Color getChartBackground() { + if (isDarkMode()) { + if (chartBackgroundDark == null) { + chartBackgroundDark = new Color(null, 30, 30, 35); + } + return chartBackgroundDark; + } else { + if (chartBackground == null) { + chartBackground = new Color(null, 255, 255, 255); + } + return chartBackground; + } + } + + public static Color getChartForeground() { + if (isDarkMode()) { + if (chartForegroundDark == null) { + chartForegroundDark = new Color(null, 200, 200, 210); + } + return chartForegroundDark; + } else { + if (chartForeground == null) { + chartForeground = new Color(null, 0, 0, 0); + } + return chartForeground; + } + } + + public static Color getChartGridNarrow() { + if (isDarkMode()) { + if (chartGridNarrowDark == null) { + chartGridNarrowDark = new Color(null, 55, 55, 70); + } + return chartGridNarrowDark; + } else { + if (chartGridNarrow == null) { + chartGridNarrow = new Color(null, 220, 228, 255); + } + return chartGridNarrow; + } + } + + public static Color getChartGridWide() { + if (isDarkMode()) { + if (chartGridWideDark == null) { + chartGridWideDark = new Color(null, 70, 70, 90); + } + return chartGridWideDark; + } else { + if (chartGridWide == null) { + chartGridWide = new Color(null, 200, 208, 255); + } + return chartGridWide; + } + } + + public static Color getXLogIgnoreArea() { + if (isDarkMode()) { + if (xlogIgnoreAreaDark == null) { + xlogIgnoreAreaDark = new Color(null, 45, 45, 50); + } + return xlogIgnoreAreaDark; + } else { + if (xlogIgnoreArea == null) { + xlogIgnoreArea = new Color(null, 234, 234, 234); + } + return xlogIgnoreArea; + } + } + + public static Color getChartBorderColor() { + if (isDarkMode()) { + if (chartBorderDark == null) { + chartBorderDark = new Color(null, 100, 100, 120); + } + return chartBorderDark; + } else { + if (chartBorder == null) { + chartBorder = new Color(null, 0, 0, 0); + } + return chartBorder; + } + } + + public static Color getFilteredBackground() { + if (isDarkMode()) { + if (filteredBackgroundDark == null) { + filteredBackgroundDark = new Color(null, 30, 40, 50); + } + return filteredBackgroundDark; + } else { + if (filteredBackground == null) { + filteredBackground = new Color(null, 240, 255, 255); + } + return filteredBackground; + } + } + + public static Color getAxisGridColor() { + if (isDarkMode()) { + if (axisGridDark == null) { + axisGridDark = new Color(null, 60, 60, 75); + } + return axisGridDark; + } else { + if (axisGrid == null) { + axisGrid = new Color(null, 200, 200, 200); + } + return axisGrid; + } + } } diff --git a/scouter.client/src/scouter/client/views/EQCommonView.java b/scouter.client/src/scouter/client/views/EQCommonView.java index 025ff4986..1e3b6c884 100644 --- a/scouter.client/src/scouter/client/views/EQCommonView.java +++ b/scouter.client/src/scouter/client/views/EQCommonView.java @@ -1,5 +1,5 @@ /* - * Copyright 2015 the original author or authors. + * Copyright 2015 the original author or authors. * @https://github.com/scouter-project/scouter * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -65,19 +65,35 @@ public abstract class EQCommonView extends ViewPart implements RefreshThread.Ref private static int MINIMUM_UNIT_HEIGHT = 20; protected RefreshThread thread; - + protected Canvas canvas; private long lastFetchedTime; protected Set valueSet = new TreeSet(new EqDataComparator()); private int unitHeight; - + private Image ibuffer; - + private ScrolledComposite scroll; int winYSize; Rectangle area; + + // Dark mode colors (cached on UI thread in createPartControl) + private Color dmBackground; + private Color dmForeground; + private Color dmSubText; + private Color dmBorder; + private Color dmGrid; + public void createPartControl(final Composite parent) { - parent.setBackground(ColorUtil.getInstance().getColor(SWT.COLOR_WHITE)); + boolean dark = ColorUtil.isDarkMode(); + if (dark) { + dmBackground = new Color(null, 30, 30, 35); + dmForeground = new Color(null, 200, 200, 210); + dmSubText = new Color(null, 140, 140, 150); + dmBorder = new Color(null, 100, 100, 120); + dmGrid = new Color(null, 55, 55, 70); + } + parent.setBackground(dark ? dmBackground : ColorUtil.getInstance().getColor(SWT.COLOR_WHITE)); parent.setBackgroundMode(SWT.INHERIT_FORCE); parent.setLayout(UIUtil.formLayout(0, 0)); GridLayout layout = new GridLayout(1, true); @@ -159,7 +175,12 @@ private void drawEQImage(GC gc) { static Color black = ColorUtil.getInstance().getColor(SWT.COLOR_BLACK); static Color red = ColorUtil.getInstance().getColor(SWT.COLOR_RED); static Color dark_gary = ColorUtil.getInstance().getColor(SWT.COLOR_GRAY); - + + private boolean isDark() { return dmBackground != null; } + private Color fg() { return isDark() ? dmForeground : black; } + private Color subText() { return isDark() ? dmSubText : dark_gary; } + private Color border() { return isDark() ? dmBorder : black; } + protected void buildBars() { long now = TimeUtil.getCurrentTime(); if ((now - lastDrawTime) < REFRESH_INTERVAL || area == null) { @@ -173,6 +194,10 @@ protected void buildBars() { Image img = new Image(null, width, height); GC gc = new GC(img); try { + if (isDark()) { + gc.setBackground(dmBackground); + gc.fillRectangle(0, 0, width, height); + } lastDrawTime = now; double maxValue = 0; ArrayList list = new ArrayList(); @@ -201,14 +226,14 @@ protected void buildBars() { } // draw horizontal line - gc.setForeground(XLogViewPainter.color_grid_narrow); + gc.setForeground(isDark() ? dmGrid : new Color(null, 220, 228, 255)); gc.setLineStyle(SWT.LINE_DOT); for (int i = AXIS_PADDING + unitHeight; i <= height - unitHeight; i = i + unitHeight) { gc.drawLine(0, i, width, i); } - + // draw axis line - gc.setForeground(black); + gc.setForeground(fg()); gc.setLineStyle(SWT.LINE_SOLID); int verticalLineX = 6; gc.drawLine(verticalLineX, AXIS_PADDING , verticalLineX, height); @@ -221,7 +246,7 @@ protected void buildBars() { for (int i = 0; i < datas.length; i++) { // draw objName String objName = datas[i].displayName; - gc.setForeground(dark_gary); + gc.setForeground(subText()); gc.setFont(verdana10Italic); int strWidth = gc.stringExtent(objName).x; while (groundWidth <= (strWidth+5)) { @@ -232,7 +257,7 @@ protected void buildBars() { int y1 = AXIS_PADDING + (unitHeight * i) + ((unitHeight - (gc.stringExtent(objName).y + 2))); gc.drawString(objName, x1, y1, true); if (datas[i].isAlive == false) { - gc.setForeground(dark_gary); + gc.setForeground(subText()); gc.setLineWidth(2); gc.drawLine(x1-1, y1 + (gc.stringExtent(objName).y / 2), x1 + gc.stringExtent(objName).x + 1, y1 + (gc.stringExtent(objName).y / 2)); } @@ -297,7 +322,7 @@ protected void buildBars() { // draw count text if (datas[i].isAlive) { gc.setFont(verdana10Bold); - gc.setForeground(black); + gc.setForeground(fg()); String v = Long.toString(total); String all = "(" + Long.toString(asd.act3) + " / " + Long.toString(asd.act2) + " / " + Long.toString(asd.act1) + ")"; @@ -325,27 +350,27 @@ protected void buildBars() { gc.drawString(v, xaxis, yaxis, true); xaxis += gc.stringExtent(v).x + 1; - gc.setForeground(black); + gc.setForeground(fg()); v = " / "; gc.drawString(v, xaxis, yaxis, true); - + xaxis += gc.stringExtent(v).x + 1; gc.setForeground(ColorUtil.getInstance().ac2); v = Long.toString(asd.act2); gc.drawString(v, xaxis, yaxis, true); - + xaxis += gc.stringExtent(v).x + 1; - gc.setForeground(black); + gc.setForeground(fg()); v = " / "; gc.drawString(v, xaxis, yaxis, true); - + xaxis += gc.stringExtent(v).x + 1; gc.setForeground(ColorUtil.getInstance().ac1); v = Long.toString(asd.act1); gc.drawString(v, xaxis, yaxis, true); - + xaxis += gc.stringExtent(v).x + 1; - gc.setForeground(black); + gc.setForeground(fg()); v = ")"; gc.drawString(v, xaxis, yaxis, true); } @@ -353,7 +378,7 @@ protected void buildBars() { } // draw scale text - gc.setForeground(black); + gc.setForeground(fg()); gc.setFont(verdana7); int max = (int) maxValue; String v = Integer.toString(max); @@ -376,7 +401,7 @@ protected void buildBars() { private void drawNemo(GC gc, Color background, int x, int y, int width, int height) { gc.setBackground(background); gc.fillRectangle(x, y, width, height); - gc.setForeground(black); + gc.setForeground(border()); gc.drawRectangle(x, y, width, height); } diff --git a/scouter.client/src/scouter/client/views/VerticalEQCommonView.java b/scouter.client/src/scouter/client/views/VerticalEQCommonView.java index cce36458d..4892ac734 100644 --- a/scouter.client/src/scouter/client/views/VerticalEQCommonView.java +++ b/scouter.client/src/scouter/client/views/VerticalEQCommonView.java @@ -1,5 +1,5 @@ /* - * Copyright 2015 the original author or authors. + * Copyright 2015 the original author or authors. * @https://github.com/scouter-project/scouter * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -83,8 +83,24 @@ public abstract class VerticalEQCommonView extends ViewPart implements RefreshTh private ScrolledComposite scroll; int winXSize; Rectangle area; + + // Dark mode colors (cached on UI thread in createPartControl) + private Color dmBackground; + private Color dmForeground; + private Color dmSubText; + private Color dmBorder; + private Color dmGrid; + public void createPartControl(final Composite parent) { - parent.setBackground(ColorUtil.getInstance().getColor(SWT.COLOR_WHITE)); + boolean dark = ColorUtil.isDarkMode(); + if (dark) { + dmBackground = new Color(null, 30, 30, 35); + dmForeground = new Color(null, 200, 200, 210); + dmSubText = new Color(null, 140, 140, 150); + dmBorder = new Color(null, 100, 100, 120); + dmGrid = new Color(null, 55, 55, 70); + } + parent.setBackground(dark ? dmBackground : ColorUtil.getInstance().getColor(SWT.COLOR_WHITE)); parent.setBackgroundMode(SWT.INHERIT_FORCE); parent.setLayout(UIUtil.formLayout(0, 0)); GridLayout layout = new GridLayout(1, true); @@ -167,8 +183,13 @@ private void drawEQImage(GC gc) { static Color red = ColorUtil.getInstance().getColor(SWT.COLOR_RED); static Color dark_gary = ColorUtil.getInstance().getColor(SWT.COLOR_GRAY); + private boolean isDark() { return dmBackground != null; } + private Color fg() { return isDark() ? dmForeground : black; } + private Color subText() { return isDark() ? dmSubText : dark_gary; } + private Color border() { return isDark() ? dmBorder : black; } + private Map objectNameImageMap = new HashMap(); - + protected void buildBars() { long now = TimeUtil.getCurrentTime(); if ((now - lastDrawTime) < REFRESH_INTERVAL || area == null) { @@ -181,8 +202,12 @@ protected void buildBars() { int height = area.height > 50 ? area.height : 50; Image img = new Image(null, width, height); GC gc = new GC(img); - + try { + if (isDark()) { + gc.setBackground(dmBackground); + gc.fillRectangle(0, 0, width, height); + } lastDrawTime = now; double maxValue = 0; ArrayList list = new ArrayList(); @@ -211,14 +236,14 @@ protected void buildBars() { } // draw horizontal line - gc.setForeground(XLogViewPainter.color_grid_narrow); + gc.setForeground(isDark() ? dmGrid : new Color(null, 220, 228, 255)); gc.setLineStyle(SWT.LINE_DOT); for (int i = AXIS_PADDING + unitWidth; i <= width - unitWidth; i = i + unitWidth) { gc.drawLine(i, 0, i, height); } - + // draw axis line - gc.setForeground(black); + gc.setForeground(fg()); gc.setLineStyle(SWT.LINE_SOLID); int verticalLineX = 6; int verticalLineY = 6; @@ -233,7 +258,7 @@ protected void buildBars() { for (int i = 0; i < datas.length; i++) { // draw objName String objName = datas[i].displayName; - gc.setForeground(dark_gary); + gc.setForeground(subText()); gc.setFont(verdana10Italic); int strWidth = gc.stringExtent(objName).x; while (groundWidth <= (strWidth+5)) { @@ -252,7 +277,7 @@ protected void buildBars() { gc.drawImage(objectNameImageMap.get(objName), x, y); if (datas[i].isAlive == false) { - gc.setForeground(dark_gary); + gc.setForeground(subText()); gc.setLineWidth(2); gc.drawLine(x + (gc.stringExtent(objName).y / 2), y - 1, x + (gc.stringExtent(objName).y / 2), y + gc.stringExtent(objName).x + 1); } @@ -318,7 +343,7 @@ protected void buildBars() { // draw count text if (datas[i].isAlive) { gc.setFont(verdana10Bold); - gc.setForeground(black); + gc.setForeground(fg()); String v = Long.toString(total); String all = "(" + Long.toString(asd.act3) + " / " + Long.toString(asd.act2) + " / " + Long.toString(asd.act1) + ")"; @@ -345,7 +370,7 @@ protected void buildBars() { gc.drawString(v, xaxis, yaxis, true); xaxis += gc.stringExtent(v).x + 1; - gc.setForeground(black); + gc.setForeground(fg()); v = " / "; gc.drawString(v, xaxis, yaxis, true); @@ -355,7 +380,7 @@ protected void buildBars() { gc.drawString(v, xaxis, yaxis, true); xaxis += gc.stringExtent(v).x + 1; - gc.setForeground(black); + gc.setForeground(fg()); v = " / "; gc.drawString(v, xaxis, yaxis, true); @@ -365,7 +390,7 @@ protected void buildBars() { gc.drawString(v, xaxis, yaxis, true); xaxis += gc.stringExtent(v).x + 1; - gc.setForeground(black); + gc.setForeground(fg()); v = ")"; gc.drawString(v, xaxis, yaxis, true); } @@ -373,7 +398,7 @@ protected void buildBars() { } // draw scale text - gc.setForeground(black); + gc.setForeground(fg()); gc.setFont(verdana7); int max = (int) maxValue; String v = Integer.toString(max); @@ -396,7 +421,7 @@ protected void buildBars() { private void drawNemo(GC gc, Color background, int x, int y, int width, int height) { gc.setBackground(background); gc.fillRectangle(x, y, width, height); - gc.setForeground(black); + gc.setForeground(border()); gc.drawRectangle(x, y, width, height); } diff --git a/scouter.client/src/scouter/client/workspace/SwitchWorkspaceDialog.java b/scouter.client/src/scouter/client/workspace/SwitchWorkspaceDialog.java new file mode 100644 index 000000000..2a4dd5af4 --- /dev/null +++ b/scouter.client/src/scouter/client/workspace/SwitchWorkspaceDialog.java @@ -0,0 +1,271 @@ +/* + * Copyright 2015 the original author or authors. + * @https://github.com/scouter-project/scouter + * + * 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 scouter.client.workspace; + +import org.eclipse.jface.dialogs.IDialogConstants; +import org.eclipse.jface.dialogs.InputDialog; +import org.eclipse.jface.dialogs.MessageDialog; +import org.eclipse.jface.dialogs.TitleAreaDialog; +import org.eclipse.jface.window.Window; +import org.eclipse.swt.SWT; +import org.eclipse.swt.events.SelectionAdapter; +import org.eclipse.swt.events.SelectionEvent; +import org.eclipse.swt.layout.GridData; +import org.eclipse.swt.layout.GridLayout; +import org.eclipse.swt.widgets.*; + +import java.io.File; +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.List; + +public class SwitchWorkspaceDialog extends TitleAreaDialog { + + private Text pathText; + private Table workspaceTable; + private String selectedPath; + private final String currentWorkspacePath; + + public SwitchWorkspaceDialog(Shell parentShell) { + super(parentShell); + setShellStyle(getShellStyle() | SWT.RESIZE); + currentWorkspacePath = WorkspaceManager.getInstance().getCurrentWorkspacePath(); + } + + @Override + protected void configureShell(Shell newShell) { + super.configureShell(newShell); + newShell.setText("Switch Workspace"); + newShell.setSize(600, 450); + } + + @Override + protected Control createDialogArea(Composite parent) { + setTitle("Switch Workspace"); + setMessage("Select a workspace to switch to. The application will restart."); + + Composite area = (Composite) super.createDialogArea(parent); + Composite container = new Composite(area, SWT.NONE); + container.setLayoutData(new GridData(GridData.FILL_BOTH)); + container.setLayout(new GridLayout(3, false)); + + Label pathLabel = new Label(container, SWT.NONE); + pathLabel.setText("Workspace:"); + + pathText = new Text(container, SWT.BORDER); + pathText.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); + + Button browseButton = new Button(container, SWT.PUSH); + browseButton.setText(".."); + browseButton.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent e) { + DirectoryDialog dialog = new DirectoryDialog(getShell()); + dialog.setText("Select Workspace Directory"); + dialog.setMessage("Select the workspace directory"); + String dir = dialog.open(); + if (dir != null) { + pathText.setText(dir); + } + } + }); + + Label tableLabel = new Label(container, SWT.NONE); + tableLabel.setText("Recent Workspaces:"); + GridData tableLabelGd = new GridData(); + tableLabelGd.horizontalSpan = 3; + tableLabel.setLayoutData(tableLabelGd); + + workspaceTable = new Table(container, SWT.BORDER | SWT.FULL_SELECTION | SWT.SINGLE); + workspaceTable.setHeaderVisible(true); + workspaceTable.setLinesVisible(true); + GridData tableGd = new GridData(GridData.FILL_BOTH); + tableGd.horizontalSpan = 2; + workspaceTable.setLayoutData(tableGd); + + TableColumn nameCol = new TableColumn(workspaceTable, SWT.NONE); + nameCol.setText("Name"); + nameCol.setWidth(120); + + TableColumn pathCol = new TableColumn(workspaceTable, SWT.NONE); + pathCol.setText("Path"); + pathCol.setWidth(250); + + TableColumn lastUsedCol = new TableColumn(workspaceTable, SWT.NONE); + lastUsedCol.setText("Last Used"); + lastUsedCol.setWidth(150); + + workspaceTable.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent e) { + int idx = workspaceTable.getSelectionIndex(); + if (idx >= 0) { + TableItem item = workspaceTable.getItem(idx); + String path = item.getData("path").toString(); + if (!isCurrentWorkspace(path)) { + pathText.setText(path); + } + } + } + }); + + Composite buttonPanel = new Composite(container, SWT.NONE); + buttonPanel.setLayout(new GridLayout(1, false)); + buttonPanel.setLayoutData(new GridData(SWT.FILL, SWT.TOP, false, false)); + + Button newButton = new Button(buttonPanel, SWT.PUSH); + newButton.setText("New..."); + newButton.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, true, false)); + newButton.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent e) { + createNewWorkspace(); + } + }); + + Button removeButton = new Button(buttonPanel, SWT.PUSH); + removeButton.setText("Remove"); + removeButton.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, true, false)); + removeButton.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent e) { + removeSelectedWorkspace(); + } + }); + + refreshTable(); + + return area; + } + + @Override + protected void createButtonsForButtonBar(Composite parent) { + createButton(parent, IDialogConstants.OK_ID, "Switch", true); + createButton(parent, IDialogConstants.CANCEL_ID, IDialogConstants.CANCEL_LABEL, false); + } + + @Override + protected void okPressed() { + String path = pathText.getText().trim(); + if (path.isEmpty()) { + setErrorMessage("Please enter or select a workspace path."); + return; + } + if (isCurrentWorkspace(path)) { + setErrorMessage("This is the current workspace. Please select a different one."); + return; + } + File dir = new File(path); + if (!dir.exists()) { + boolean create = MessageDialog.openQuestion(getShell(), "Create Workspace", + "The directory does not exist. Create it?"); + if (create) { + if (!dir.mkdirs()) { + setErrorMessage("Failed to create directory: " + path); + return; + } + } else { + return; + } + } + WorkspaceManager.getInstance().addWorkspace(path, dir.getName()); + WorkspaceManager.getInstance().setLastUsed(path); + selectedPath = path; + super.okPressed(); + } + + public String getSelectedPath() { + return selectedPath; + } + + private void refreshTable() { + workspaceTable.removeAll(); + SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); + List list = WorkspaceManager.getInstance().getWorkspaceList(); + String normalizedCurrent = normalizePath(currentWorkspacePath); + for (WorkspaceInfo info : list) { + TableItem item = new TableItem(workspaceTable, SWT.NONE); + String name = info.getDisplayName(); + boolean isCurrent = normalizePath(info.getPath()).equals(normalizedCurrent); + if (isCurrent) { + name = name + " (current)"; + } + item.setText(0, name); + item.setText(1, info.getPath()); + item.setText(2, info.getLastUsed() > 0 ? sdf.format(new Date(info.getLastUsed())) : ""); + item.setData("path", info.getPath()); + if (isCurrent) { + item.setGrayed(true); + } + } + } + + private void createNewWorkspace() { + DirectoryDialog dirDialog = new DirectoryDialog(getShell()); + dirDialog.setText("Select New Workspace Directory"); + dirDialog.setMessage("Choose a directory for the new workspace"); + String dir = dirDialog.open(); + if (dir == null) return; + + InputDialog nameDialog = new InputDialog(getShell(), "Workspace Name", + "Enter a display name for this workspace:", new File(dir).getName(), null); + if (nameDialog.open() == Window.OK) { + String name = nameDialog.getValue().trim(); + if (name.isEmpty()) { + name = new File(dir).getName(); + } + File dirFile = new File(dir); + if (!dirFile.exists()) { + dirFile.mkdirs(); + } + WorkspaceManager.getInstance().addWorkspace(dir, name); + refreshTable(); + pathText.setText(dir); + } + } + + private void removeSelectedWorkspace() { + int idx = workspaceTable.getSelectionIndex(); + if (idx < 0) { + setErrorMessage("Please select a workspace to remove."); + return; + } + TableItem item = workspaceTable.getItem(idx); + String path = item.getData("path").toString(); + if (isCurrentWorkspace(path)) { + setErrorMessage("Cannot remove the current workspace."); + return; + } + WorkspaceManager.getInstance().removeWorkspace(path); + refreshTable(); + pathText.setText(""); + setErrorMessage(null); + } + + private boolean isCurrentWorkspace(String path) { + return normalizePath(path).equals(normalizePath(currentWorkspacePath)); + } + + private String normalizePath(String path) { + if (path == null) return ""; + if (path.endsWith("/") || path.endsWith(File.separator)) { + path = path.substring(0, path.length() - 1); + } + return path; + } +} diff --git a/scouter.client/src/scouter/client/workspace/WorkspaceInfo.java b/scouter.client/src/scouter/client/workspace/WorkspaceInfo.java new file mode 100644 index 000000000..af2f943c7 --- /dev/null +++ b/scouter.client/src/scouter/client/workspace/WorkspaceInfo.java @@ -0,0 +1,54 @@ +/* + * Copyright 2015 the original author or authors. + * @https://github.com/scouter-project/scouter + * + * 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 scouter.client.workspace; + +public class WorkspaceInfo { + private String path; + private String displayName; + private long lastUsed; + + public WorkspaceInfo(String path, String displayName, long lastUsed) { + this.path = path; + this.displayName = displayName; + this.lastUsed = lastUsed; + } + + public String getPath() { + return path; + } + + public void setPath(String path) { + this.path = path; + } + + public String getDisplayName() { + return displayName; + } + + public void setDisplayName(String displayName) { + this.displayName = displayName; + } + + public long getLastUsed() { + return lastUsed; + } + + public void setLastUsed(long lastUsed) { + this.lastUsed = lastUsed; + } +} diff --git a/scouter.client/src/scouter/client/workspace/WorkspaceManager.java b/scouter.client/src/scouter/client/workspace/WorkspaceManager.java new file mode 100644 index 000000000..531053fca --- /dev/null +++ b/scouter.client/src/scouter/client/workspace/WorkspaceManager.java @@ -0,0 +1,203 @@ +/* + * Copyright 2015 the original author or authors. + * @https://github.com/scouter-project/scouter + * + * 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 scouter.client.workspace; + +import org.eclipse.core.runtime.Platform; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import java.util.Properties; + +public class WorkspaceManager { + + private static WorkspaceManager instance; + + private static final String CONFIG_FILE = System.getProperty("user.home") + + File.separator + ".scouter" + File.separator + ".scouter-workspaces.properties"; + private static final String KEY_COUNT = "workspace.count"; + private static final String KEY_PATH_PREFIX = "workspace.path."; + private static final String KEY_NAME_PREFIX = "workspace.name."; + private static final String KEY_LAST_USED_PREFIX = "workspace.lastUsed."; + + public static synchronized WorkspaceManager getInstance() { + if (instance == null) { + instance = new WorkspaceManager(); + } + return instance; + } + + private WorkspaceManager() { + } + + public List getWorkspaceList() { + List list = new ArrayList<>(); + Properties props = loadProperties(); + int count = Integer.parseInt(props.getProperty(KEY_COUNT, "0")); + for (int i = 0; i < count; i++) { + String path = props.getProperty(KEY_PATH_PREFIX + i); + String name = props.getProperty(KEY_NAME_PREFIX + i, ""); + long lastUsed = Long.parseLong(props.getProperty(KEY_LAST_USED_PREFIX + i, "0")); + if (path != null && !path.isEmpty()) { + list.add(new WorkspaceInfo(path, name, lastUsed)); + } + } + return list; + } + + public void addWorkspace(String path, String displayName) { + List list = getWorkspaceList(); + for (WorkspaceInfo info : list) { + if (normalizePath(info.getPath()).equals(normalizePath(path))) { + return; + } + } + list.add(new WorkspaceInfo(path, displayName, System.currentTimeMillis())); + saveWorkspaceList(list); + } + + public void removeWorkspace(String path) { + List list = getWorkspaceList(); + list.removeIf(info -> normalizePath(info.getPath()).equals(normalizePath(path))); + saveWorkspaceList(list); + } + + public void deleteWorkspace(String path) { + removeWorkspace(path); + deleteDirectory(new File(path)); + } + + public void setLastUsed(String path) { + List list = getWorkspaceList(); + for (WorkspaceInfo info : list) { + if (normalizePath(info.getPath()).equals(normalizePath(path))) { + info.setLastUsed(System.currentTimeMillis()); + break; + } + } + saveWorkspaceList(list); + } + + public String getLastUsedWorkspacePath() { + List list = getWorkspaceList(); + if (list.isEmpty()) { + return null; + } + WorkspaceInfo lastUsed = null; + for (WorkspaceInfo info : list) { + if (lastUsed == null || info.getLastUsed() > lastUsed.getLastUsed()) { + lastUsed = info; + } + } + return lastUsed != null ? lastUsed.getPath() : null; + } + + public String getCurrentWorkspacePath() { + try { + return Platform.getInstanceLocation().getURL().getFile(); + } catch (Exception e) { + return ""; + } + } + + public void registerCurrentWorkspace(String workspacePath) { + String path = normalizePath(workspacePath); + List list = getWorkspaceList(); + boolean found = false; + for (WorkspaceInfo info : list) { + if (normalizePath(info.getPath()).equals(path)) { + info.setLastUsed(System.currentTimeMillis()); + found = true; + break; + } + } + if (!found) { + String name = new File(path).getName(); + if (name.isEmpty()) { + name = "Default"; + } + list.add(new WorkspaceInfo(path, name, System.currentTimeMillis())); + } + saveWorkspaceList(list); + } + + public String getDisplayName(String path) { + List list = getWorkspaceList(); + for (WorkspaceInfo info : list) { + if (normalizePath(info.getPath()).equals(normalizePath(path))) { + return info.getDisplayName(); + } + } + String name = new File(path).getName(); + return name.isEmpty() ? "Default" : name; + } + + private void saveWorkspaceList(List list) { + Properties props = new Properties(); + props.setProperty(KEY_COUNT, String.valueOf(list.size())); + for (int i = 0; i < list.size(); i++) { + WorkspaceInfo info = list.get(i); + props.setProperty(KEY_PATH_PREFIX + i, info.getPath()); + props.setProperty(KEY_NAME_PREFIX + i, info.getDisplayName()); + props.setProperty(KEY_LAST_USED_PREFIX + i, String.valueOf(info.getLastUsed())); + } + File configFile = new File(CONFIG_FILE); + configFile.getParentFile().mkdirs(); + try (FileOutputStream fos = new FileOutputStream(configFile)) { + props.store(fos, "Scouter Workspace List"); + } catch (IOException e) { + e.printStackTrace(); + } + } + + private Properties loadProperties() { + Properties props = new Properties(); + File file = new File(CONFIG_FILE); + if (file.exists()) { + try (FileInputStream fis = new FileInputStream(file)) { + props.load(fis); + } catch (IOException e) { + e.printStackTrace(); + } + } + return props; + } + + private String normalizePath(String path) { + if (path == null) return ""; + if (path.endsWith("/") || path.endsWith(File.separator)) { + path = path.substring(0, path.length() - 1); + } + return path; + } + + private void deleteDirectory(File dir) { + if (dir.isDirectory()) { + File[] children = dir.listFiles(); + if (children != null) { + for (File child : children) { + deleteDirectory(child); + } + } + } + dir.delete(); + } +} diff --git a/scouter.client/src/scouter/client/xlog/ImageCache.java b/scouter.client/src/scouter/client/xlog/ImageCache.java index e53763ec2..e8c31c027 100644 --- a/scouter.client/src/scouter/client/xlog/ImageCache.java +++ b/scouter.client/src/scouter/client/xlog/ImageCache.java @@ -89,7 +89,7 @@ private Image createXPImage5(RGB rgb) { for (int i = 0; i < 5; i++) { gcc.drawLine(i, 0, i, 4); } - gcc.setForeground(ColorUtil.getInstance().getColor("white")); + gcc.setForeground(ColorUtil.getChartBackground()); gcc.drawPoint(1, 0); gcc.drawPoint(4, 1); gcc.drawPoint(0, 3); @@ -135,4 +135,21 @@ private Image createObjectImage(RGB rgb) { gcc.dispose(); return xp; } + + public synchronized void clearCache() { + for (Image img : xLogDotMap.values()) { + if (img != null && !img.isDisposed()) { + img.dispose(); + } + } + xLogDotMap.clear(); + if (errorXpDot != null && !errorXpDot.isDisposed()) { + errorXpDot.dispose(); + } + errorXpDot = null; + if (errorXpDotLight != null && !errorXpDotLight.isDisposed()) { + errorXpDotLight.dispose(); + } + errorXpDotLight = null; + } } diff --git a/scouter.client/src/scouter/client/xlog/views/XLogFullProfileView.java b/scouter.client/src/scouter/client/xlog/views/XLogFullProfileView.java index 1b63c9709..bc81c13aa 100644 --- a/scouter.client/src/scouter/client/xlog/views/XLogFullProfileView.java +++ b/scouter.client/src/scouter/client/xlog/views/XLogFullProfileView.java @@ -51,6 +51,7 @@ import org.eclipse.ui.PartInitException; import org.eclipse.ui.part.ViewPart; import scouter.client.Activator; +import scouter.client.util.ColorUtil; import scouter.client.Images; import scouter.client.model.TextProxy; import scouter.client.util.ExUtil; @@ -143,7 +144,7 @@ public void createPartControl(Composite parent) { man = getViewSite().getActionBars().getToolBarManager(); sashForm = new SashForm(parent, SWT.HORIZONTAL); - sashForm.setBackground(parent.getDisplay().getSystemColor(SWT.COLOR_GRAY)); + sashForm.setBackground(ColorUtil.isDarkMode() ? ColorUtil.getChartBackground() : parent.getDisplay().getSystemColor(SWT.COLOR_GRAY)); sashForm.SASH_WIDTH = 1; mainComposite = new Composite(sashForm, SWT.NONE); @@ -159,7 +160,12 @@ public void createPartControl(Composite parent) { }else{ header.setFont(new Font(null, "Courier New", 10, SWT.NORMAL)); } - header.setBackgroundImage(Activator.getImage("icons/grid.jpg")); + if (ColorUtil.isDarkMode()) { + header.setBackground(ColorUtil.getChartBackground()); + header.setForeground(ColorUtil.getChartForeground()); + } else { + header.setBackgroundImage(Activator.getImage("icons/grid.jpg")); + } Composite centerComp = new Composite(mainComposite, SWT.NONE); @@ -319,7 +325,12 @@ public void widgetDefaultSelected(SelectionEvent e) { }else{ text.setFont(new Font(null, "Courier New", 10, SWT.NORMAL)); } - text.setBackgroundImage(Activator.getImage("icons/grid.jpg")); + if (ColorUtil.isDarkMode()) { + text.setBackground(ColorUtil.getChartBackground()); + text.setForeground(ColorUtil.getChartForeground()); + } else { + text.setBackgroundImage(Activator.getImage("icons/grid.jpg")); + } text.addKeyListener(adapter); text.addKeyListener(new KeyListener() { public void keyReleased(KeyEvent e) { diff --git a/scouter.client/src/scouter/client/xlog/views/XLogProfileView.java b/scouter.client/src/scouter/client/xlog/views/XLogProfileView.java index 0fb61ec5c..121aa20f4 100644 --- a/scouter.client/src/scouter/client/xlog/views/XLogProfileView.java +++ b/scouter.client/src/scouter/client/xlog/views/XLogProfileView.java @@ -36,6 +36,7 @@ import org.eclipse.ui.PlatformUI; import org.eclipse.ui.part.ViewPart; import scouter.client.Activator; +import scouter.client.util.ColorUtil; import scouter.client.Images; import scouter.client.constants.HelpConstants; import scouter.client.model.XLogData; @@ -80,7 +81,12 @@ public void createPartControl(Composite parent) { }else{ text.setFont(new Font(null, "Courier New", 10, SWT.NORMAL)); } - text.setBackgroundImage(Activator.getImage("icons/grid.jpg")); + if (ColorUtil.isDarkMode()) { + text.setBackground(ColorUtil.getChartBackground()); + text.setForeground(ColorUtil.getChartForeground()); + } else { + text.setBackgroundImage(Activator.getImage("icons/grid.jpg")); + } IToolBarManager man = getViewSite().getActionBars().getToolBarManager(); man.add(openSqlSummaryDialog); diff --git a/scouter.client/src/scouter/client/xlog/views/XLogThreadProfileView.java b/scouter.client/src/scouter/client/xlog/views/XLogThreadProfileView.java index ee302dfc6..4d3b34e49 100644 --- a/scouter.client/src/scouter/client/xlog/views/XLogThreadProfileView.java +++ b/scouter.client/src/scouter/client/xlog/views/XLogThreadProfileView.java @@ -32,6 +32,7 @@ import org.eclipse.ui.part.ViewPart; import scouter.client.Activator; +import scouter.client.util.ColorUtil; import scouter.client.model.XLogData; import scouter.client.xlog.ProfileText; import scouter.client.xlog.actions.OpenXLogProfileJob; @@ -64,7 +65,12 @@ public void createPartControl(Composite parent) { }else{ text.setFont(new Font(null, "Courier New", 10, SWT.NORMAL)); } - text.setBackgroundImage(Activator.getImage("icons/grid.jpg")); + if (ColorUtil.isDarkMode()) { + text.setBackground(ColorUtil.getChartBackground()); + text.setForeground(ColorUtil.getChartForeground()); + } else { + text.setBackgroundImage(Activator.getImage("icons/grid.jpg")); + } } public void setInput(final XLogData data, Step[] steps, long threadId, int serverId) { diff --git a/scouter.client/src/scouter/client/xlog/views/XLogViewCommon.java b/scouter.client/src/scouter/client/xlog/views/XLogViewCommon.java index 7628460ab..5c75aa0e6 100644 --- a/scouter.client/src/scouter/client/xlog/views/XLogViewCommon.java +++ b/scouter.client/src/scouter/client/xlog/views/XLogViewCommon.java @@ -50,6 +50,7 @@ import scouter.client.server.ServerManager; import scouter.client.threads.ObjectSelectManager; import scouter.client.threads.ObjectSelectManager.IObjectCheckListener; +import scouter.client.util.ColorUtil; import scouter.client.util.ExUtil; import scouter.client.util.ImageUtil; import scouter.client.xlog.XLogFilterStatus; @@ -181,6 +182,7 @@ public void run() { canvas = new Canvas(parent, SWT.DOUBLE_BUFFERED); canvas.setLayout(new GridLayout()); + canvas.setBackground(ColorUtil.getChartBackground()); mouse = new XLogViewMouse(twdata, canvas); viewPainter = new XLogViewPainter(twdata, mouse, this); diff --git a/scouter.client/src/scouter/client/xlog/views/XLogViewPainter.java b/scouter.client/src/scouter/client/xlog/views/XLogViewPainter.java index 46d34ed68..48cccb6e7 100644 --- a/scouter.client/src/scouter/client/xlog/views/XLogViewPainter.java +++ b/scouter.client/src/scouter/client/xlog/views/XLogViewPainter.java @@ -53,13 +53,8 @@ import java.util.Enumeration; public class XLogViewPainter { - public static Color color_black = new Color(null, 0, 0, 0); - public static Color color_white = new Color(null, 255, 255, 255); - public static Color color_grid_narrow = new Color(null, 220, 228, 255); - public static Color color_grid_wide = new Color(null, 200, 208, 255); public static Color color_blue = new Color(null, 0, 0, 255); public static Color color_red = new Color(null, 255, 0, 0); - public static Color ignore_area = new Color(null, 234, 234, 234); public long xTimeRange = DateUtil.MILLIS_PER_MINUTE * 5; public long originalRange = xTimeRange; @@ -212,24 +207,24 @@ private void draw(GC gc, int work_w, int work_h) { paintedEndTime = time_end; - gc.setForeground(color_black); + gc.setForeground(ColorUtil.getChartForeground()); if (filter_hash != new XLogFilterStatus().hashCode()) { - gc.setBackground(ColorUtil.getInstance().getColor("azure")); + gc.setBackground(ColorUtil.getFilteredBackground()); } else { - gc.setBackground(color_white); + gc.setBackground(ColorUtil.getChartBackground()); } gc.fillRectangle(0, 0, work_w, work_h); if (yAxisMode == XLogYAxisEnum.ELAPSED) { int ignoreMs = PManager.getInstance().getInt(PreferenceConstants.P_XLOG_IGNORE_TIME); if (yValueMin == 0 && ignoreMs > 0) { - gc.setBackground(ignore_area); + gc.setBackground(ColorUtil.getXLogIgnoreArea()); int chart_igreno_h = (int) ((ignoreMs / (yValueMax * 1000)) * chart_h); if (chart_igreno_h > chart_h) { chart_igreno_h = chart_h; } gc.fillRoundRectangle(chart_x, 30 + chart_h - chart_igreno_h, chart_w + 5, chart_igreno_h + 5, 1, 1); - gc.setBackground(color_white); + gc.setBackground(ColorUtil.getChartBackground()); } } @@ -241,12 +236,12 @@ private void draw(GC gc, int work_w, int work_h) { for (double yValue = 0; yValue <= valueRange; yValue += yUnit) { int y = (int) (chart_y + chart_h - chart_h * yValue / valueRange); - gc.setForeground(color_grid_narrow); + gc.setForeground(ColorUtil.getChartGridNarrow()); gc.drawLine(chart_x, y, (int) (chart_x + chart_w), y); String s = FormatUtil.print(new Double(yValue + yValueMin), "#,##0.00"); - gc.setForeground(color_black); - gc.drawString(s, chart_x - (15 + s.length() * 6), y - 5); + gc.setForeground(ColorUtil.getChartForeground()); + gc.drawString(s, chart_x - (15 + s.length() * 6), y - 5, true); } double xUnit = ChartUtil.getSplitTimeUnit(xTimeRange, chart_w); @@ -260,19 +255,19 @@ private void draw(GC gc, int work_w, int work_h) { boolean labelOn = (xi++ % 5 == labelOnNum); if (labelOn) { - gc.setForeground(color_grid_wide); + gc.setForeground(ColorUtil.getChartGridWide()); gc.setLineStyle(SWT.LINE_SOLID); } else { - gc.setForeground(color_grid_narrow); + gc.setForeground(ColorUtil.getChartGridNarrow()); gc.setLineStyle(SWT.LINE_DOT); } int x = (int) (chart_x + (chart_w * timeDelta / xTimeRange)); gc.drawLine(x, chart_y, x, chart_y + chart_h); if (labelOn) { - gc.setForeground(color_black); + gc.setForeground(ColorUtil.getChartForeground()); String s = FormatUtil.print(new Date(time_start + (long) timeDelta), xLabelFormat); - gc.drawString(s, x - 25, chart_y + chart_h + 5 + 5); + gc.drawString(s, x - 25, chart_y + chart_h + 5 + 5, true); } } @@ -295,20 +290,20 @@ private void draw(GC gc, int work_w, int work_h) { private void drawZoomMode(GC gc, int chart_x, int chart_y, int chart_w, long stime, long etime) { String cntText = "Zoom Mode(" + DateUtil.format(stime, "HH:mm") + "~" + DateUtil.format(etime, "HH:mm") + ")"; - gc.drawText(cntText, chart_x + chart_w - 140, chart_y - 20); + gc.drawText(cntText, chart_x + chart_w - 140, chart_y - 20, true); } private void drawTxCount(GC gc, int chart_x, int chart_w, int chart_y) { gc.setFont(null); String cntText = " Count : " + FormatUtil.print(new Long(count), "#,##0"); int strLen = gc.stringExtent(cntText).x; - gc.drawText(cntText, chart_x + chart_w - strLen - 10, chart_y - 20); + gc.drawText(cntText, chart_x + chart_w - strLen - 10, chart_y - 20, true); } - + private void drawYaxisDescription(GC gc, int chart_x, int chart_y) { gc.setFont(null); String desc = " " + yAxisMode.getDesc(); - gc.drawText(desc, chart_x, chart_y - 20); + gc.drawText(desc, chart_x, chart_y - 20, true); } private void drawChartBorder(GC gc, int chart_sx, int chart_sy, int chart_w, int chart_h) { @@ -319,7 +314,7 @@ private void drawChartBorder(GC gc, int chart_sx, int chart_sy, int chart_w, int gc.setLineStyle(SWT.LINE_SOLID); gc.setLineWidth(1); - gc.setForeground(color_black); + gc.setForeground(ColorUtil.getChartBorderColor()); gc.drawRoundRectangle(chart_sx, chart_sy, chart_w + 5, chart_h + 5, 1, 1); }