001/**
002 * Please feel free to use any fragment of the code in this file that you need in your own
003 * work. As far as I am concerned, it's in the public domain. No permission is necessary
004 * or required. Credit is always appreciated if you use a large chunk or base a
005 * significant product on one of my examples, but that's not required either.
006 *
007 * This code is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
008 * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
009 * PURPOSE.
010 *
011 * --- Joseph A. Huwaldt
012 */
013package jahuwaldt.swing;
014
015import java.io.*;
016import java.util.Locale;
017import static java.util.Objects.nonNull;
018import javax.swing.JFrame;
019import javax.swing.JRootPane;
020
021/**
022 * A set of utilities that are used by my programs when running under MacOS only. These
023 * static methods provide Mac specific functionality but all the methods in it can be
024 * called safely from any platform. On non-Mac systems, the worst that will happen when
025 * calling these methods is nothing and sometimes appropriate non-Mac behavior is
026 * provided.
027 *
028 * <p> Modified by: Joseph A. Huwaldt </p>
029 *
030 * @author Joseph A. Huwaldt Date: September 3, 2000
031 * @version February 23, 2025
032 */
033public class MacOSUtilities {
034
035    private static final boolean IS_MACOS;
036
037    static {
038        String OS = System.getProperty("os.name", "generic").toLowerCase(Locale.ENGLISH);
039        IS_MACOS = (OS.contains("mac") || OS.contains("darwin"));
040    }
041
042    /**
043     * Returns true if this program is running in any MacOS 8/9/X environment, false is
044     * returned otherwise.
045     *
046     * @return <code>true</code> if this program is running in any MacOS environment.
047     */
048    public static boolean isMacOS() {
049        return IS_MACOS;
050    }
051
052    /**
053     * Returns the a File reference to the specified resource in the MacOS application
054     * bundle's Resource directory. This method takes advantage of the application
055     * bundle's used by MacOS to hide resources from the user. If not running on a MacOS
056     * system, then the resource is searched for in three locations. 1st the directory
057     * that the application is running in. 2nd the default application name supplied is
058     * combined with ".app/Contents/Resources/" + resourceName and searched for in the
059     * current directory. This is how a MacOS application bundle would appear on a non-Mac
060     * system. Third, the default application name is combined with
061     * ".app/Contents/Resources/" + resourceName and searched for in the install directory
062     * (if it can be determined).
063     *
064     * @param resourceName   The name of the resource file.
065     * @param defaultAppName The default application name to use if not running on MacOS.
066     * @return A File reference to the specified resource in the MacOS X application
067     * bundle's Resource directory.
068     * @throws FileNotFoundException if the specified resource file could not be found.
069     */
070    public static File getResource(String resourceName, String defaultAppName) throws FileNotFoundException {
071
072        //      First look for the resource in the application directory.
073        File file = new File(resourceName);
074
075        if (!file.exists()) {
076
077            //  Try looking in a MacOS style application bundle in current (working) directory.
078            String sep = System.getProperty("file.separator");
079            StringBuilder path = new StringBuilder(defaultAppName);
080            path.append(".app");
081            path.append(sep);
082            path.append("Contents");
083            path.append(sep);
084            path.append("Resources");
085            path.append(sep);
086            path.append(resourceName);
087            file = new File(path.toString());
088
089            //  Try looking for MacOS style application bundle in install directory (if known).
090            if (!file.exists()) {
091                String installPath = System.getProperty("lax.root.install.dir");
092                if (installPath == null || installPath.equals(""))
093                    installPath = System.getProperty("user.dir");
094
095                installPath = installPath.replace(File.separatorChar, '/');
096                if (!installPath.endsWith("/"))
097                    path.insert(0, "/");
098                path.insert(0, installPath);
099                file = new File(path.toString());
100
101                //      Couldn't find the resource file anywhere.
102                if (!file.exists())
103                    throw new FileNotFoundException(path.toString());
104            }
105
106        }
107
108        return file;
109    }
110
111    /**
112     * Returns the a File reference to the specified resource in the specified
113     * sub-directory of the application bundle's Resource directory. This method takes
114     * advantage of the application bundle's used by MacOS to hide resources from the
115     * user. If not running on a MacOS system, then the resource is searched for in three
116     * locations. 1st the directory that the application is running in. 2nd the default
117     * application name supplied is combined with ".app/Contents/Resources/" +
118     * resourceName and searched for in the current directory. This is how a MacOS
119     * application bundle would appear on a non-Mac system. Third, the default application
120     * name is combined with ".app/Contents/Resources/" + resourceName and searched for in
121     * the install directory (if it can be determined).
122     *
123     * @param resourceName   The name of the resource file.
124     * @param subDirName     The name of the sub-directory under Resources.
125     * @param defaultAppName The default application name to use if not running on MacOS.
126     * @return A File reference to the specified resource in the MacOS X application
127     * bundle's Resource directory.
128     * @throws FileNotFoundException if the specified resource file could not be found.
129     */
130    public static File getResource(String resourceName, String subDirName, String defaultAppName)
131            throws FileNotFoundException {
132
133        //      First look for the resource in the application directory.
134        String sep = System.getProperty("file.separator");
135        StringBuffer path = new StringBuffer(subDirName);
136        path.append(sep);
137        path.append(resourceName);
138        File file = new File(path.toString());
139
140        if (!file.exists()) {
141
142            //  Try looking in a MacOS style application bundle in current (working) directory.
143            path = new StringBuffer(defaultAppName);
144            path.append(".app");
145            path.append(sep);
146            path.append("Contents");
147            path.append(sep);
148            path.append("Resources");
149            path.append(sep);
150            path.append(subDirName);
151            path.append(sep);
152            path.append(resourceName);
153            file = new File(path.toString());
154
155            //  Try looking for MacOS style application bundle in install directory (if known).
156            if (!file.exists()) {
157                String installPath = System.getProperty("lax.root.install.dir");
158                if (installPath == null || installPath.equals(""))
159                    installPath = System.getProperty("user.dir");
160
161                installPath = installPath.replace(File.separatorChar, '/');
162                if (!installPath.endsWith("/"))
163                    path.insert(0, "/");
164                path.insert(0, installPath);
165                file = new File(path.toString());
166
167                //      Couldn't find the resource file anywhere.
168                if (!file.exists())
169                    throw new FileNotFoundException(path.toString());
170            }
171        }
172
173        return file;
174    }
175
176    /**
177     * Return a reference to the MacOS application support folder. This method throws a
178     * <code>FileNotFoundException</code> on other platforms, which is the same as what
179     * this method does on MacOS when the folder can't be found.
180     *
181     * @param appName The name of this application.
182     * @param create  <code>true</code> if the folder should be created if it doesn't
183     *                exist.
184     * @return the application support folder object, or null
185     * @exception FileNotFoundException when the folder can't be found
186     */
187    public static File appSupportFolder(String appName, boolean create) throws FileNotFoundException {
188        if (!isMacOS())
189            throw new FileNotFoundException();
190
191        //  Get the user's home directory path.
192        File home = new File(System.getProperty("user.home"));
193
194        //  Construct the path to the Application Support folder for this application.
195        File appSpt = new File(home, "Library/Application Support");
196        if (!appSpt.exists() || !appSpt.canWrite())
197            throw new FileNotFoundException("Can't find \"Library/Application Support\" folder.");
198
199        //  Construct the application support folder for this application.
200        File folder = new File(appSpt, appName);
201
202        //  If the folder doesn't exist, create it.
203        if (create && !folder.exists())
204            folder.mkdir();
205
206        return folder;
207    }
208
209    /**
210     * Sets the "Window.documentModified" client property on the frame's root pane to the
211     * value indicated. On a Mac, the modified-document mark is standardized as a black
212     * dot in the center of the red close button on the title bar. The proxy icon, if
213     * present, is also dimmed if <code>modified</code> is set to true.
214     *
215     * @param window   The JFrame to have the modified mark set on.
216     * @param modified Set to true to indicate that the document represented by frame is
217     *                 modified. Set to false to indicate that it has not been modified
218     *                 since it was last created, read in, or saved.
219     */
220    public static void setModifiedMark(JFrame window, boolean modified) {
221        JRootPane root = window.getRootPane();
222        root.putClientProperty("Window.documentModified", modified);
223    }
224
225    /**
226     * Return the state of the "Window.documentModified" client property for the specified
227     * frame. On a Mac, the modified-document mark is standardized as a black dot in the
228     * center of the red close button on the title bar. It is used to indicate that a
229     * document has been modified.
230     *
231     * @param window The frame to have the document modified client property read from.
232     * @return The state of the "Window.documentModified" client property for the
233     * specified frame.
234     */
235    public static boolean isMarkedModified(JFrame window) {
236        JRootPane root = window.getRootPane();
237        Object prop = root.getClientProperty("Window.documentModified");
238        if (nonNull(prop) && prop instanceof Boolean) {
239            return (boolean) prop;
240        }
241        return Boolean.FALSE;
242    }
243
244    /**
245     * Set's the title bar proxy icon. On a Mac, document windows have a proxy icon to the
246     * left of the title text. Users can click and drag the icon to other windows or
247     * right-click/command-click to get a path to the document.
248     *
249     * @param window The JFrame to have a proxy icon added to the title bar.
250     * @param file   The file who's icon should be added to the title bar. The Mac
251     *               automatically finds the correct icon for the file or folder.
252     */
253    public static void setProxyIcon(JFrame window, File file) {
254        JRootPane root = window.getRootPane();
255        root.putClientProperty("Window.documentFile", file);
256    }
257
258    /**
259     * Return the contents of the "Window.documentFile" client property (which can return
260     * null). On a Mac, document windows have a proxy icon to the left of the title text.
261     * Users can click and drag the icon to other windows or right-click/command-click to
262     * get a path to the document. The file who's icon is used as the proxy icon, is
263     * retrieved from a frame with this method.
264     *
265     * @param window The JFrame to have the proxy icon file retrieved.
266     * @return The file who's icon is used as the proxy icon or null if there is none.
267     */
268    public static File getProxyIcon(JFrame window) {
269        JRootPane root = window.getRootPane();
270        return (File) root.getClientProperty("Window.documentFile");
271    }
272}